给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
提示:
2 <= nums.length <= 104-109 <= nums[i] <= 109-109 <= target <= 109进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?
方法一:哈希表
我们可以用哈希表 存放数组值以及对应的下标。
遍历数组 nums,当发现 target - nums[i] 在哈希表中,说明找到了目标值,返回 target - nums[i] 的下标以及 即可。
时间复杂度 ,空间复杂度 。其中 是数组 nums 的长度。
class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> m = new HashMap<>(); for (int i = 0;; ++i) { int x = nums[i]; int y = target - x; if (m.containsKey(y)) { return new int[] {m.get(y), i}; } m.put(x, i); } } }
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0] 输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出:[8,9,9,9,0,0,0,1]
提示:
[1, 100] 内0 <= Node.val <= 9方法一:模拟
我们同时遍历两个链表 和 ,并使用变量 表示当前是否有进位。
每次遍历时,我们取出对应链表的当前位,计算它们与进位 的和,然后更新进位的值,最后将当前位的值加入答案链表。如果两个链表都遍历完了,并且进位为 时,遍历结束。
最后我们返回答案链表的头节点即可。
时间复杂度 ,其中 和 分别为两个链表的长度。我们需要遍历两个链表的全部位置,而处理每个位置只需要 的时间。忽略答案的空间消耗,空间复杂度 。
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(0); int carry = 0; ListNode cur = dummy; while (l1 != null || l2 != null || carry != 0) { int s = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + carry; carry = s / 10; cur.next = new ListNode(s % 10); cur = cur.next; l1 = l1 == null ? null : l1.next; l2 = l2 == null ? null : l2.next; } return dummy.next; } }
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104s 由英文字母、数字、符号和空格组成方法一:双指针 + 哈希表
定义一个哈希表记录当前窗口内出现的字符,记 和 分别表示不重复子串的开始位置和结束位置,无重复字符子串的最大长度记为 ans。
遍历字符串 s 的每个字符 ,我们记为 。若 窗口内存在 ,则 循环向右移动,更新哈希表,直至 窗口不存在 c,循环结束。将 c 加入哈希表中,此时 窗口内不含重复元素,更新 ans 的最大值。
最后返回 ans 即可。
时间复杂度 ,其中 表示字符串 s 的长度。
双指针算法模板:
for (int i = 0, j = 0; i < n; ++i) { while (j < i && check(j, i)) { ++j; } // 具体问题的逻辑 }
class Solution { public int lengthOfLongestSubstring(String s) { Set<Character> ss = new HashSet<>(); int i = 0, ans = 0; for (int j = 0; j < s.length(); ++j) { char c = s.charAt(j); while (ss.contains(c)) { ss.remove(s.charAt(i++)); } ss.add(c); ans = Math.max(ans, j - i + 1); } return ans; } }
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2] 输出:2.00000 解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4] 输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
nums1.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-106 <= nums1[i], nums2[i] <= 106方法一:分治
本题限制了时间复杂度为 ,看到这个时间复杂度,自然而然的想到了应该使用二分查找法来求解。那么回顾一下中位数的定义,如果某个有序数组长度是奇数,那么其中位数就是最中间那个,如果是偶数,那么就是最中间两个数字的平均值。这里对于两个有序数组也是一样的,假设两个有序数组的长度分别为 和 ,由于两个数组长度之和 的奇偶不确定,因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。为了简化代码,不分情况讨论,我们使用一个小 trick,我们分别找第 和 个,然后求其平均值即可,这对奇偶数均适用。假如 为奇数的话,那么其实 和 的值相等,相当于两个相同的数字相加再除以 2,还是其本身。
这里我们需要定义一个函数来在两个有序数组中找到第 个元素,下面重点来看如何实现找到第 个元素。
首先,为了避免产生新的数组从而增加时间复杂度,我们使用两个变量 和 分别来标记数组 nums1 和 nums2 的起始位置。然后来处理一些边界问题,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有就是如果 的话,那么我们只要比较 nums1 和 nums2 的起始位置 和 上的数字就可以了。
难点就在于一般的情况怎么处理?因为我们需要在两个有序数组中找到第 个元素,为了加快搜索的速度,我们要使用二分法,对 二分,意思是我们需要分别在 nums1 和 nums2 中查找第 个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第 个数字,所以我们需要先检查一下,数组中到底存不存在第 个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第 个数字,那么我们就淘汰另一个数字的前 个数字即可。有没有可能两个数组都不存在第 个数字呢,这道题里是不可能的,因为我们的 不是任意给的,而是给的 的中间值,所以必定至少会有一个数组是存在第 个数字的。
最后是二分法的核心,比较这两个数组的第 小的数字 midVal1 和 midVal2 的大小,如果第一个数组的第 个数字小的话,那么说明我们要找的数字肯定不在 nums1 中的前 个数字,所以我们可以将其淘汰,将 nums1 的起始位置向后移动 个,并且此时的 也自减去 ,调用递归。反之,我们淘汰 nums2 中的前 个数字,并将 nums2 的起始位置向后移动 个,并且此时的 也自减去 ,调用递归即可。
实际是比较两个数组中的第 个数字哪一个可能到达最后合并后排序数组中的第 个元素的位置,其中小的那个数字注定不可能到达,所以可以直接将小的元素对应的数组的前 个数字淘汰。
时间复杂度 ,其中 和 是两个数组的长度。
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int left = (m + n + 1) / 2; int right = (m + n + 2) / 2; return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0; } private int findKth(int[] nums1, int i, int[] nums2, int j, int k) { if (i >= nums1.length) { return nums2[j + k - 1]; } if (j >= nums2.length) { return nums1[i + k - 1]; } if (k == 1) { return Math.min(nums1[i], nums2[j]); } int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE; int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE; if (midVal1 < midVal2) { return findKth(nums1, i + k / 2, nums2, j, k - k / 2); } return findKth(nums1, i, nums2, j + k / 2, k - k / 2); } }
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd" 输出:"bb"
提示:
1 <= s.length <= 1000s 仅由数字和英文字母组成方法一:动态规划
设 表示字符串 是否为回文串。
2 时,只要 ,那么 就为 true。时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
方法二:枚举回文中间点
我们可以枚举回文中间点,向两边扩散,找到最长的回文串。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
class Solution { public String longestPalindrome(String s) { int n = s.length(); boolean[][] dp = new boolean[n][n]; int mx = 1, start = 0; for (int j = 0; j < n; ++j) { for (int i = 0; i <= j; ++i) { if (j - i < 2) { dp[i][j] = s.charAt(i) == s.charAt(j); } else { dp[i][j] = dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j); } if (dp[i][j] && mx < j - i + 1) { mx = j - i + 1; start = i; } } } return s.substring(start, start + mx); } }
class Solution { private String s; private int n; public String longestPalindrome(String s) { this.s = s; n = s.length(); int start = 0, mx = 1; for (int i = 0; i < n; ++i) { int a = f(i, i); int b = f(i, i + 1); int t = Math.max(a, b); if (mx < t) { mx = t; start = i - ((t - 1) >> 1); } } return s.substring(start, start + mx); } private int f(int l, int r) { while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) { --l; ++r; } return r - l - 1; } }
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:
P A H N A P L S I I G Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入:s = "PAYPALISHIRING", numRows = 3 输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4 输出:"PINALSIGYAHRPI" 解释: P I N A L S I G Y A H R P I
示例 3:
输入:s = "A", numRows = 1 输出:"A"
提示:
1 <= s.length <= 1000s 由英文字母(小写和大写)、',' 和 '.' 组成1 <= numRows <= 1000方法一:模拟
我们用一个二维数组 来模拟 字形排列的过程,其中 表示第 行第 列的字符。初始时 ,另外我们定义一个方向变量 ,初始时 ,表示向上走。
我们从左到右遍历字符串 ,每次遍历到一个字符 ,将其追加到 中,如果此时 或者 ,说明当前字符位于 字形排列的拐点,我们将 的值反转,即 。接下来,我们将 的值更新为 ,即向上或向下移动一行。继续遍历下一个字符,直到遍历完字符串 ,我们返回 中所有行拼接后的字符串即可。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
class Solution { public String convert(String s, int numRows) { if (numRows == 1) { return s; } StringBuilder[] g = new StringBuilder[numRows]; Arrays.setAll(g, k -> new StringBuilder()); int i = 0, k = -1; for (char c : s.toCharArray()) { g[i].append(c); if (i == 0 || i == numRows - 1) { k = -k; } i += k; } return String.join("", g); } }
class Solution { public String convert(String s, int numRows) { if (numRows == 1) { return s; } StringBuilder ans = new StringBuilder(); int group = 2 * numRows - 2; for (int i = 1; i <= numRows; i++) { int interval = i == numRows ? group : 2 * numRows - 2 * i; int idx = i - 1; while (idx < s.length()) { ans.append(s.charAt(idx)); idx += interval; interval = group - interval; if (interval == 0) { interval = group; } } } return ans.toString(); } }
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。
示例 1:
输入:x = 123 输出:321
示例 2:
输入:x = -123 输出:-321
示例 3:
输入:x = 120 输出:21
示例 4:
输入:x = 0 输出:0
提示:
-231 <= x <= 231 - 1方法一:数学
我们不妨记 和 分别为 和 ,则 的反转结果 需要满足 。
我们可以通过不断地对 取余来获取 的最后一位数字 ,并将 添加到 的末尾。在添加 之前,我们需要判断 是否溢出。即判断 是否在 的范围内。
若 ,那么需要满足 ,即 。整理得 。
下面我们讨论上述不等式成立的条件:
综上,当 时,不等式成立的充要条件是 。
同理,当 时,不等式成立的充要条件是 。
因此,我们可以通过判断 是否在 的范围内来判断 是否溢出。若溢出,则返回 。否则,将 添加到 的末尾,然后将 的最后一位数字去除,即 。
时间复杂度 ,其中 为 的绝对值。空间复杂度 。
class Solution { public int reverse(int x) { int ans = 0; for (; x != 0; x /= 10) { if (ans < Integer.MIN_VALUE / 10 || ans > Integer.MAX_VALUE / 10) { return 0; } ans = ans * 10 + x % 10; } return ans; } }
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
函数 myAtoi(string s) 的算法如下:
0 。必要时更改符号(从步骤 2 开始)。[−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1 。注意:
' ' 。示例 1:
输入:s = "42"
输出:42
解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"42"(读入 "42")
^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。
示例 2:
输入:s = " -42"
输出:-42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
^
第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数)
^
第 3 步:" -42"(读入 "42")
^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。
示例 3:
输入:s = "4193 with words"
输出:4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。
提示:
0 <= s.length <= 200s 由英文字母(大写和小写)、数字(0-9)、' '、'+'、'-' 和 '.' 组成遍历字符串,注意做溢出处理。
class Solution { public int myAtoi(String s) { if (s == null) return 0; int n = s.length(); if (n == 0) return 0; int i = 0; while (s.charAt(i) == ' ') { // 仅包含空格 if (++i == n) return 0; } int sign = 1; if (s.charAt(i) == '-') sign = -1; if (s.charAt(i) == '-' || s.charAt(i) == '+') ++i; int res = 0, flag = Integer.MAX_VALUE / 10; for (; i < n; ++i) { // 非数字,跳出循环体 if (s.charAt(i) < '0' || s.charAt(i) > '9') break; // 溢出判断 if (res > flag || (res == flag && s.charAt(i) > '7')) return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; res = res * 10 + (s.charAt(i) - '0'); } return sign * res; } }
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
121 是回文,而 123 不是。示例 1:
输入:x = 121 输出:true
示例 2:
输入:x = -121 输出:false 解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入:x = 10 输出:false 解释:从右向左读, 为 01 。因此它不是一个回文数。
提示:
-231 <= x <= 231 - 1进阶:你能不将整数转为字符串来解决这个问题吗?
方法一:反转一半数字
我们先判断特殊情况:
false;false;我们将 的后半部分反转,与前半部分进行比较,如果相等,那么 是回文数,否则 不是回文数。
举个例子,例如 ,我们可以将数字后半部分从 “21” 反转为 “12”,并将其与前半部分 “12” 进行比较,因为二者相等,我们得知数字 是回文。
让我们看看如何将后半部分反转。
对于数字 ,如果执行 ,我们将得到最后一位数字 ,要得到倒数第二位数字,我们可以先通过除以 将最后一位数字从 中移除,,再求出上一步结果除以 的余数,,就可以得到倒数第二位数字。
如果继续这个过程,我们将得到更多位数的反转数字。
通过将最后一位数字不断地累乘到取出数字的变量 上,我们可以得到以相反顺序的数字。
在代码实现上,我们可以反复“取出” 的最后一位数字,并将其“添加”到 的后面,循环直到 ,如果此时 ,或者 ,那么 就是回文数。
时间复杂度 ,其中 是 。对于每次迭代,我们会将输入除以 ,因此时间复杂度为 。空间复杂度 。
class Solution { public boolean isPalindrome(int x) { if (x < 0 || (x > 0 && x % 10 == 0)) { return false; } int y = 0; for (; y < x; x /= 10) { y = y * 10 + x % 10; } return x == y || x == y / 10; } }
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符'*' 匹配零个或多个前面的那一个元素所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab", p = ".*" 输出:true 解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
提示:
1 <= s.length <= 201 <= p.length <= 20s 只包含从 a-z 的小写字母。p 只包含从 a-z 的小写字母,以及字符 . 和 *。* 时,前面都匹配到有效的字符方法一:记忆化搜索
我们设计一个函数 ,表示从 的第 个字符开始,和 的第 个字符开始是否匹配。那么答案就是 。
函数 的计算过程如下:
'*',我们可以选择匹配 个 字符,那么就是 。如果此时 并且 和 匹配,那么我们可以选择匹配 个 字符,那么就是 。'*',那么如果 并且 和 匹配,那么就是 。否则匹配失败。过程中,我们可以使用记忆化搜索,避免重复计算。
时间复杂度 ,空间复杂度 。其中 和 分别是 和 的长度。
方法二:动态规划
我们可以将方法一中的记忆化搜索转换为动态规划。
定义 表示字符串 的前 个字符和字符串 的前 个字符是否匹配。那么答案就是 。初始化 ,表示空字符串和空正则表达式是匹配的。
与方法一类似,我们可以分情况来讨论。
'*',那么我们可以选择匹配 个 字符,那么就是 。如果此时 和 匹配,那么我们可以选择匹配 个 字符,那么就是 。'*',那么如果 和 匹配,那么就是 。否则匹配失败。时间复杂度 ,空间复杂度 。其中 和 分别是 和 的长度。
class Solution { private Boolean[][] f; private String s; private String p; private int m; private int n; public boolean isMatch(String s, String p) { m = s.length(); n = p.length(); f = new Boolean[m + 1][n + 1]; this.s = s; this.p = p; return dfs(0, 0); } private boolean dfs(int i, int j) { if (j >= n) { return i == m; } if (f[i][j] != null) { return f[i][j]; } boolean res = false; if (j + 1 < n && p.charAt(j + 1) == '*') { res = dfs(i, j + 2) || (i < m && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') && dfs(i + 1, j)); } else { res = i < m && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') && dfs(i + 1, j + 1); } return f[i][j] = res; } }
class Solution { public boolean isMatch(String s, String p) { int m = s.length(), n = p.length(); boolean[][] f = new boolean[m + 1][n + 1]; f[0][0] = true; for (int i = 0; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (p.charAt(j - 1) == '*') { f[i][j] = f[i][j - 2]; if (i > 0 && (p.charAt(j - 2) == '.' || p.charAt(j - 2) == s.charAt(i - 1))) { f[i][j] |= f[i - 1][j]; } } else if (i > 0 && (p.charAt(j - 1) == '.' || p.charAt(j - 1) == s.charAt(i - 1))) { f[i][j] = f[i - 1][j - 1]; } } } return f[m][n]; } }
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:

输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1] 输出:1
提示:
n == height.length2 <= n <= 1050 <= height[i] <= 104方法一:双指针
一开始,我们考虑相距最远的两个柱子所能容纳水的容量。水的宽度是两根柱子之间的距离,而水的高度取决于两根柱子之间较短的那个。
当前柱子是最两侧的柱子,水的宽度最大,其他的组合,水的宽度都比这个小。不妨假设左侧柱子的高度小于等于右侧柱子的高度,那么水的高度就是左侧柱子的高度。如果我们移动右侧柱子,那么水的宽度就减小了,而水的高度却不会增加,因此水的容量一定减少。所以我们移动左侧柱子,更新最大容量。
循环此过程,直到两个柱子相遇。
时间复杂度 ,其中 是数组 height 的长度。空间复杂度 。
class Solution { public int maxArea(int[] height) { int i = 0, j = height.length - 1; int ans = 0; while (i < j) { int t = Math.min(height[i], height[j]) * (j - i); ans = Math.max(ans, t); if (height[i] < height[j]) { ++i; } else { --j; } } return ans; } }
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。给你一个整数,将其转为罗马数字。
示例 1:
输入: num = 3 输出: "III"
示例 2:
输入: num = 4 输出: "IV"
示例 3:
输入: num = 9 输出: "IX"
示例 4:
输入: num = 58 输出: "LVIII" 解释: L = 50, V = 5, III = 3.
示例 5:
输入: num = 1994 输出: "MCMXCIV" 解释: M = 1000, CM = 900, XC = 90, IV = 4.
提示:
1 <= num <= 3999方法一:贪心
我们可以先将所有可能的符号 和对应的数值 列出来,然后从大到小枚举每个数值 ,每次尽可能多地使用该数值对应的符号 ,直到数字 变为 。
时间复杂度为 ,空间复杂度为 。其中 为符号的个数。
class Solution { public String intToRoman(int num) { List<String> cs = Arrays.asList("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"); List<Integer> vs = Arrays.asList(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1); StringBuilder ans = new StringBuilder(); for (int i = 0, n = cs.size(); i < n; ++i) { while (num >= vs.get(i)) { num -= vs.get(i); ans.append(cs.get(i)); } } return ans.toString(); } }
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。给定一个罗马数字,将其转换成整数。
示例 1:
输入: s = "III" 输出: 3
示例 2:
输入: s = "IV" 输出: 4
示例 3:
输入: s = "IX" 输出: 9
示例 4:
输入: s = "LVIII" 输出: 58 解释: L = 50, V= 5, III = 3.
示例 5:
输入: s = "MCMXCIV" 输出: 1994 解释: M = 1000, CM = 900, XC = 90, IV = 4.
提示:
1 <= s.length <= 15s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内方法一:哈希表 + 模拟
我们先用哈希表 记录每个字符对应的数值,然后从左到右遍历字符串 ,如果当前字符对应的数值小于右边字符对应的数值,则减去当前字符对应的数值,否则加上当前字符对应的数值。
时间复杂度 ,空间复杂度 。其中 和 分别为字符串 的长度和字符集的大小。
class Solution { public int romanToInt(String s) { String cs = "IVXLCDM"; int[] vs = {1, 5, 10, 50, 100, 500, 1000}; Map<Character, Integer> d = new HashMap<>(); for (int i = 0; i < vs.length; ++i) { d.put(cs.charAt(i), vs[i]); } int n = s.length(); int ans = d.get(s.charAt(n - 1)); for (int i = 0; i < n - 1; ++i) { int sign = d.get(s.charAt(i)) < d.get(s.charAt(i + 1)) ? -1 : 1; ans += sign * d.get(s.charAt(i)); } return ans; } }
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入:strs = ["flower","flow","flight"] 输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"] 输出:"" 解释:输入不存在公共前缀。
提示:
1 <= strs.length <= 2000 <= strs[i].length <= 200strs[i] 仅由小写英文字母组成方法一:字符比较
我们以第一个字符串 为基准,依次比较后面的字符串的第 个字符是否与 的第 个字符相同,如果相同则继续比较下一个字符,否则返回 的前 个字符。
遍历结束,说明所有字符串的前 个字符都相同,返回 即可。
时间复杂度 ,其中 和 分别为字符串数组的长度以及字符串的最小长度。空间复杂度 。
class Solution { public String longestCommonPrefix(String[] strs) { int n = strs.length; for (int i = 0; i < strs[0].length(); ++i) { for (int j = 1; j < n; ++j) { if (strs[j].length() <= i || strs[j].charAt(i) != strs[0].charAt(i)) { return strs[0].substring(0, i); } } } return strs[0]; } }
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000-105 <= nums[i] <= 105方法一:排序 + 双指针
我们注意到,题目不要求我们按照顺序返回三元组,因此我们不妨先对数组进行排序,这样就可以方便地跳过重复的元素。
接下来,我们枚举三元组的第一个元素 ,其中 。对于每个 ,我们可以通过维护两个指针 和 ,从而找到满足 的 和 。在枚举的过程中,我们需要跳过重复的元素,以避免出现重复的三元组。
具体判断逻辑如下:
如果 并且 ,则说明当前枚举的元素与上一个元素相同,我们可以直接跳过,因为不会产生新的结果。
如果 ,则说明当前枚举的元素大于 ,则三数之和必然无法等于 ,结束枚举。
否则,我们令左指针 ,右指针 ,当 时,执行循环,计算三数之和 ,并与 比较:
枚举结束后,我们即可得到三元组的答案。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { /* 将数组排序,这样相同的数字就会挨在一起,方便进行去重; 从左到右枚举数组中的每个数字作为第一个数 a; 在 a 右边的区域内使用双指针 left 和 right,找出所有满足 a + nums[left] + nums[right] == 0 的三元组,并将其加入结果集; 注意去重。 */ public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new ArrayList<>(); int n = nums.length; Arrays.sort(nums); // 排序 for (int i = 0; i < n - 2; i++) { if (i > 0 && nums[i] == nums[i - 1]) { // 去重 continue; } int left = i + 1; int right = n - 1; while (left < right) { int sum = nums[i] + nums[left] + nums[right]; if (sum == 0) { result.add(Arrays.asList(nums[i], nums[left], nums[right])); left++; while (left < right && nums[left] == nums[left - 1]) { // 去重 left++; } right--; while (left < right && nums[right] == nums[right + 1]) { // 去重 right--; } } else if (sum < 0) { left++; } else { right--; } } } return result; } }
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。
示例 1:
输入:nums = [-1,2,1,-4], target = 1 输出:2 解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
示例 2:
输入:nums = [0,0,0], target = 1 输出:0
提示:
3 <= nums.length <= 1000-1000 <= nums[i] <= 1000-104 <= target <= 104方法一:排序 + 双指针
将数组排序,然后遍历数组,对于每个元素 ,我们使用指针 和 分别指向 和 ,计算三数之和,如果三数之和等于 ,则直接返回 ,否则根据与 的差值更新答案。如果三数之和大于 ,则将 向左移动一位,否则将 向右移动一位。
时间复杂度 ,空间复杂度 。其中 为数组长度。
class Solution { public int threeSumClosest(int[] nums, int target) { Arrays.sort(nums); int ans = 1 << 30; int n = nums.length; for (int i = 0; i < n; ++i) { int j = i + 1, k = n - 1; while (j < k) { int t = nums[i] + nums[j] + nums[k]; if (t == target) { return t; } if (Math.abs(t - target) < Math.abs(ans - target)) { ans = t; } if (t > target) { --k; } else { ++j; } } } return ans; } }
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "" 输出:[]
示例 3:
输入:digits = "2" 输出:["a","b","c"]
提示:
0 <= digits.length <= 4digits[i] 是范围 ['2', '9'] 的一个数字。方法一:遍历
我们先用一个数组或者哈希表存储每个数字对应的字母,然后遍历每个数字,将其对应的字母与之前的结果进行组合,得到新的结果。
时间复杂度 。其中 是输入数字的长度。
class Solution { public List<String> letterCombinations(String digits) { List<String> ans = new ArrayList<>(); if (digits.length() == 0) { return ans; } ans.add(""); String[] d = new String[] {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; for (char i : digits.toCharArray()) { String s = d[i - '2']; List<String> t = new ArrayList<>(); for (String a : ans) { for (String b : s.split("")) { t.add(a + b); } } ans = t; } return ans; } }
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < na、b、c 和 d 互不相同nums[a] + nums[b] + nums[c] + nums[d] == target你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200-109 <= nums[i] <= 109-109 <= target <= 109方法一:排序 + 双指针
该题和 0015.三数之和 相似,解法也相似。
时间复杂度为 ,空间复杂度为 ,其中 是数组的长度。
class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { int n = nums.length; if (n < 4) { return Collections.emptyList(); } Arrays.sort(nums); List<List<Integer>> res = new ArrayList<>(); for (int i = 0; i < n - 3; ++i) { if (i > 0 && nums[i] == nums[i - 1]) { continue; } for (int j = i + 1; j < n - 2; ++j) { if (j > i + 1 && nums[j] == nums[j - 1]) { continue; } int k = j + 1, l = n - 1; while (k < l) { if (nums[i] + nums[j] + nums[k] + nums[l] == target) { res.add(Arrays.asList(nums[i], nums[j], nums[k], nums[l])); ++k; --l; while (k < n && nums[k] == nums[k - 1]) { ++k; } while (l > j && nums[l] == nums[l + 1]) { --l; } } else if (nums[i] + nums[j] + nums[k] + nums[l] < target) { ++k; } else { --l; } } } } return res; } }
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1 输出:[]
示例 3:
输入:head = [1,2], n = 1 输出:[1]
提示:
sz1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz进阶:你能尝试使用一趟扫描实现吗?
方法一:快慢指针
定义两个指针 fast 和 slow,初始时都指向链表的虚拟头结点 dummy。
接着 fast 指针先向前移动 步,然后 fast 和 slow 指针同时向前移动,直到 fast 指针到达链表的末尾。此时 slow.next 指针指向的结点就是倒数第 n 个结点的前驱结点,将其删除即可。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0, head); ListNode fast = dummy, slow = dummy; while (n-- > 0) { fast = fast.next; } while (fast.next != null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; } }
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
示例 1:
输入:s = "()" 输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]" 输出:false
提示:
1 <= s.length <= 104s 仅由括号 '()[]{}' 组成方法一:栈
遍历括号字符串 ,遇到左括号时,压入当前的左括号;遇到右括号时,弹出栈顶元素(若栈为空,直接返回 false),判断是否匹配,若不匹配,直接返回 false。
也可以选择遇到左括号时,将右括号压入栈中;遇到右括号时,弹出栈顶元素(若栈为空,直接返回 false),判断是否是相等。若不匹配,直接返回 false。
两者的区别仅限于括号转换时机,一个是在入栈时,一个是在出栈时。
遍历结束,若栈为空,说明括号字符串有效,返回 true;否则,返回 false。
时间复杂度 ,空间复杂度 。其中 为括号字符串 的长度。
class Solution { public boolean isValid(String s) { Deque<Character> stk = new ArrayDeque<>(); for (char c : s.toCharArray()) { if (c == '(' || c == '{' || c == '[') { stk.push(c); } else if (stk.isEmpty() || !match(stk.pop(), c)) { return false; } } return stk.isEmpty(); } private boolean match(char l, char r) { return (l == '(' && r == ')') || (l == '{' && r == '}') || (l == '[' && r == ']'); } }
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
提示:
[0, 50]-100 <= Node.val <= 100l1 和 l2 均按 非递减顺序 排列方法一:递归
我们先判断链表 和 是否为空,若其中一个为空,则返回另一个链表。否则,我们比较 和 的头节点:
时间复杂度 ,空间复杂度 。其中 和 分别为两个链表的长度。
方法二:迭代
我们也可以用迭代的方式来实现两个排序链表的合并。
我们先定义一个虚拟头节点 ,然后循环遍历两个链表,比较两个链表的头节点,将较小的节点添加到 的末尾,直到其中一个链表为空,然后将另一个链表的剩余部分添加到 的末尾。
最后返回 即可。
时间复杂度 ,其中 和 分别为两个链表的长度。忽略答案链表的空间消耗,空间复杂度 。
class Solution { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { if (list1 == null) { return list2; } if (list2 == null) { return list1; } if (list1.val <= list2.val) { list1.next = mergeTwoLists(list1.next, list2); return list1; } else { list2.next = mergeTwoLists(list1, list2.next); return list2; } } }
class Solution { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { ListNode dummy = new ListNode(); ListNode curr = dummy; while (list1 != null && list2 != null) { if (list1.val <= list2.val) { curr.next = list1; list1 = list1.next; } else { curr.next = list2; list2 = list2.next; } curr = curr.next; } curr.next = list1 == null ? list2 : list1; return dummy.next; } }
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1 输出:["()"]
提示:
1 <= n <= 8方法一:DFS + 剪枝
题目中 的范围为 ,因此我们直接通过“暴力搜索 + 剪枝”的方式快速解决本题。
我们设计函数 dfs(l, r, t),其中 和 分别表示左括号和右括号的数量,而 表示当前的括号序列。那么我们可以得到如下的递归结构:
ans 中,直接返回;dfs(l + 1, r, t + "(");dfs(l, r + 1, t + ")")。时间复杂度 ,空间复杂度 。
class Solution { private List<String> ans = new ArrayList<>(); private int n; public List<String> generateParenthesis(int n) { this.n = n; dfs(0, 0, ""); return ans; } private void dfs(int l, int r, String t) { if (l > n || r > n || l < r) { return; } if (l == n && r == n) { ans.add(t); return; } dfs(l + 1, r, t + "("); dfs(l, r + 1, t + ")"); } }
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
示例 2:
输入:lists = [] 输出:[]
示例 3:
输入:lists = [[]] 输出:[]
提示:
k == lists.length0 <= k <= 10^40 <= lists[i].length <= 500-10^4 <= lists[i][j] <= 10^4lists[i] 按 升序 排列lists[i].length 的总和不超过 10^4朴素解法:
合并前后两个链表,结果放在后一个链表位置上,依次循环下去。最后返回链表数组的最后一个元素。
分而治之:
多个链表合并复杂,若只有两个或一个链表时,那么就如同 21. 合并两个有序链表 一样。
与归并排序同思路,不断拆散,最终合并即可。
class Solution { public ListNode mergeKLists(ListNode[] lists) { int n = lists.length; if (n == 0) { return null; } for (int i = 0; i < n - 1; ++i) { lists[i + 1] = mergeLists(lists[i], lists[i + 1]); } return lists[n - 1]; } private ListNode mergeLists(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(); ListNode cur = dummy; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; } cur = cur.next; } cur.next = l1 == null ? l2 : l1; return dummy.next; } }
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]
示例 2:
输入:head = [] 输出:[]
示例 3:
输入:head = [1] 输出:[1]
提示:
[0, 100] 内0 <= Node.val <= 100方法一:迭代
设置虚拟头节点 dummy,pre 指针初始指向 dummy,遍历链表,每次交换 pre 后面的两个节点即可。
时间复杂度为 ,空间复杂度为 ,其中 是链表的长度。
方法二:递归
时间复杂度为 ,空间复杂度为 ,其中 是链表的长度。
class Solution { public ListNode swapPairs(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode pre = dummy, cur = head; while (cur != null && cur.next != null) { ListNode t = cur.next; cur.next = t.next; t.next = cur; pre.next = t; pre = cur; cur = cur.next; } return dummy.next; } }
迭代:
递归:
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5]
示例 2:

输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
提示:
n1 <= k <= n <= 50000 <= Node.val <= 1000进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?
方法一:迭代
时间复杂度为 ,空间复杂度为 ,其中 是链表的长度。
方法二:递归
时间复杂度为 ,空间复杂度为 ,其中 是链表的长度。
class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode dummy = new ListNode(0, head); ListNode pre = dummy, cur = dummy; while (cur.next != null) { for (int i = 0; i < k && cur != null; ++i) { cur = cur.next; } if (cur == null) { return dummy.next; } ListNode t = cur.next; cur.next = null; ListNode start = pre.next; pre.next = reverseList(start); start.next = t; pre = start; cur = pre; } return dummy.next; } private ListNode reverseList(ListNode head) { ListNode pre = null, p = head; while (p != null) { ListNode q = p.next; p.next = pre; pre = p; p = q; } return pre; } }
迭代:
递归:
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。k 。判题标准:
系统会用下面的代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int k = removeDuplicates(nums); // 调用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被 通过。
示例 1:
输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应该返回新的长度2,并且原数组 nums 的前两个元素被修改为1,2。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4] 解释:函数应该返回新的长度5, 并且原数组 nums 的前五个元素被修改为0,1,2,3,4。不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 104-104 <= nums[i] <= 104nums 已按 升序 排列方法一:一次遍历
我们用一个变量 记录当前已经处理好的数组的长度,初始时 ,表示空数组。
然后我们从左到右遍历数组,对于遍历到的每个元素 ,如果 或者 ,我们就将 放到 的位置,然后 自增 。否则, 与 相同,我们直接跳过这个元素。继续遍历,直到遍历完整个数组。
这样,当遍历结束时, 中前 个元素就是我们要求的答案,且 就是答案的长度。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
补充:
原问题要求最多相同的数字最多出现 次,我们可以扩展至相同的数字最多保留 个。
相似题目:80. 删除有序数组中的重复项 II
class Solution { public int removeDuplicates(int[] nums) { int k = 0; for (int x : nums) { if (k == 0 || x != nums[k - 1]) { nums[k++] = x; } } return k; } }
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3] 解释:函数应该返回新的长度5, 并且 nums 中的前五个元素为0,1,3,0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100方法一:一次遍历
我们用变量 记录当前不等于 的元素个数。
遍历数组 ,如果当前元素 不等于 ,则将 赋值给 ,并将 自增 。
最后返回 即可。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
class Solution { public int removeElement(int[] nums, int val) { int k = 0; for (int x : nums) { if (x != val) { nums[k++] = x; } } return k; } }
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104haystack 和 needle 仅由小写英文字符组成方法一:遍历
以字符串 haystack 的每一个字符为起点与字符串 needle 进行比较,若发现能够匹配的索引,直接返回即可。
假设字符串 haystack 长度为 n,字符串 needle 长度为 m,则时间复杂度为 ,空间复杂度 。
方法二:Rabin-Karp 字符串匹配算法
Rabin-Karp 算法本质上是利用滑动窗口配合哈希函数对固定长度的字符串哈希之后进行比较,可以将比较两个字符串是否相同的时间复杂度降为 。
假设字符串 haystack 长度为 n,字符串 needle 长度为 m,则时间复杂度为 ,空间复杂度 。
方法三:KMP 字符串匹配算法
假设字符串 haystack 长度为 n,字符串 needle 长度为 m,则时间复杂度为 ,空间复杂度 。
class Solution { public int strStr(String haystack, String needle) { if ("".equals(needle)) { return 0; } int len1 = haystack.length(); int len2 = needle.length(); int p = 0; int q = 0; while (p < len1) { if (haystack.charAt(p) == needle.charAt(q)) { if (len2 == 1) { return p; } ++p; ++q; } else { p -= q - 1; q = 0; } if (q == len2) { return p - q; } } return -1; } }
遍历:
Rabin-Karp 字符串匹配算法:
给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求 不使用 乘法、除法和取余运算。
整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8 ,-2.7335 将被截断至 -2 。
返回被除数 dividend 除以除数 divisor 得到的 商 。
注意:假设我们的环境只能存储 32 位 有符号整数,其数值范围是 [−231, 231 − 1] 。本题中,如果商 严格大于 231 − 1 ,则返回 231 − 1 ;如果商 严格小于 -231 ,则返回 -231 。
示例 1:
输入: dividend = 10, divisor = 3 输出: 3 解释: 10/3 = 3.33333.. ,向零截断后得到 3 。
示例 2:
输入: dividend = 7, divisor = -3 输出: -2 解释: 7/-3 = -2.33333.. ,向零截断后得到 -2 。
提示:
-231 <= dividend, divisor <= 231 - 1divisor != 0方法一:模拟 + 快速幂
除法本质上就是减法,题目要求我们计算出两个数相除之后的取整结果,其实就是计算被除数是多少个除数加上一个小于除数的数构成的。但是一次循环只能做一次减法,效率太低会导致超时,可借助快速幂的思想进行优化。
需要注意的是,由于题目明确要求最大只能使用 32 位有符号整数,所以需要将除数和被除数同时转换为负数进行计算。因为转换正数可能会导致溢出,如当被除数为 INT32_MIN 时,转换为正数时会大于 INT32_MAX。
假设被除数为 a,除数为 b,则时间复杂度为 ,空间复杂度 。
class Solution { public int divide(int a, int b) { int sign = 1; if ((a < 0) != (b < 0)) { sign = -1; } long x = Math.abs((long) a); long y = Math.abs((long) b); long tot = 0; while (x >= y) { int cnt = 0; while (x >= (y << (cnt + 1))) { cnt++; } tot += 1L << cnt; x -= y << cnt; } long ans = sign * tot; if (ans >= Integer.MIN_VALUE && ans <= Integer.MAX_VALUE) { return (int) ans; } return Integer.MAX_VALUE; } }
给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。
s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。返回所有串联字串在 s 中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"] 输出:[0,9] 解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 输出顺序无关紧要。返回 [9,0] 也是可以的。
示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] 输出:[] 解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回一个空数组。
示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] 输出:[6,9,12] 解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。
提示:
1 <= s.length <= 1041 <= words.length <= 50001 <= words[i].length <= 30words[i] 和 s 由小写英文字母组成方法一:哈希表 + 滑动窗口
我们用哈希表 统计 中每个单词出现的次数,用哈希表 统计当前滑动窗口中每个单词出现的次数。我们记字符串 的长度为 ,字符串数组 中单词的数量为 ,每个单词的长度为 。
我们可以枚举滑动窗口的起点 ,其中 。对于每个起点,我们维护一个滑动窗口,左边界为 ,右边界为 ,滑动窗口中的单词个数为 ,另外用一个哈希表 统计滑动窗口中每个单词出现的次数。
每一次,我们提取字符串 ,如果 不在哈希表 中,说明当前滑动窗口中的单词不合法,我们将左边界 更新为 ,同时将哈希表 清空,单词个数 重置为 0。如果 在哈希表 中,说明当前滑动窗口中的单词合法,我们将单词个数 加 1,将哈希表 中 的次数加 1。如果 大于 ,说明当前滑动窗口中 出现的次数过多,我们需要将左边界 右移,直到 。如果 ,说明当前滑动窗口中的单词正好合法,我们将左边界 加入答案数组。
时间复杂度 ,空间复杂度 。其中 和 分别是字符串 和字符串数组 的长度,而 是字符串数组 中单词的长度。
class Solution { public List<Integer> findSubstring(String s, String[] words) { Map<String, Integer> cnt = new HashMap<>(); for (String w : words) { cnt.merge(w, 1, Integer::sum); } int m = s.length(), n = words.length; int k = words[0].length(); List<Integer> ans = new ArrayList<>(); for (int i = 0; i < k; ++i) { Map<String, Integer> cnt1 = new HashMap<>(); int l = i, r = i; int t = 0; while (r + k <= m) { String w = s.substring(r, r + k); r += k; if (!cnt.containsKey(w)) { cnt1.clear(); l = r; t = 0; continue; } cnt1.merge(w, 1, Integer::sum); ++t; while (cnt1.get(w) > cnt.get(w)) { String remove = s.substring(l, l + k); l += k; cnt1.merge(remove, -1, Integer::sum); --t; } if (t == n) { ans.add(l); } } } return ans; } }
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
arr = [1,2,3] 的下一个排列是 [1,3,2] 。arr = [2,3,1] 的下一个排列是 [3,1,2] 。arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3] 输出:[1,3,2]
示例 2:
输入:nums = [3,2,1] 输出:[1,2,3]
示例 3:
输入:nums = [1,1,5] 输出:[1,5,1]
提示:
1 <= nums.length <= 1000 <= nums[i] <= 100方法一:两次遍历
我们先从后往前遍历数组 ,找到第一个满足 的位置 ,那么 就是我们需要交换的元素,而 到 的元素是一个降序序列。
接下来,我们再从后往前遍历数组 ,找到第一个满足 的位置 ,然后我们交换 和 。最后,我们将 到 的元素反转,即可得到下一个排列。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { public void nextPermutation(int[] nums) { int n = nums.length; int i = n - 2; for (; i >= 0; --i) { if (nums[i] < nums[i + 1]) { break; } } if (i >= 0) { for (int j = n - 1; j > i; --j) { if (nums[j] > nums[i]) { swap(nums, i, j); break; } } } for (int j = i + 1, k = n - 1; j < k; ++j, --k) { swap(nums, j, k); } } private void swap(int[] nums, int i, int j) { int t = nums[j]; nums[j] = nums[i]; nums[i] = t; } }
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()" 输出:2 解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())" 输出:4 解释:最长有效括号子串是 "()()"
示例 3:
输入:s = "" 输出:0
提示:
0 <= s.length <= 3 * 104s[i] 为 '(' 或 ')'方法一:动态规划
我们定义 表示以 结尾的最长有效括号的长度,那么答案就是 。
当 时,字符串长度小于 ,不存在有效括号,因此 。
当 时,我们考虑以 结尾的最长有效括号的长度 :
因此,我们可以得到状态转移方程:
最后,我们只需要返回 即可。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
class Solution { public int longestValidParentheses(String s) { int n = s.length(); if (n < 2) { return 0; } int[] f = new int[n + 1]; int ans = 0; for (int i = 2; i <= n; ++i) { if (s.charAt(i - 1) == ')') { if (s.charAt(i - 2) == '(') { f[i] = f[i - 2] + 2; } else { int j = i - f[i - 1] - 1; if (j > 0 && s.charAt(j - 1) == '(') { f[i] = f[i - 1] + 2 + f[j - 1]; } } ans = Math.max(ans, f[i]); } } return ans; } }
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1
示例 3:
输入:nums = [1], target = 0 输出:-1
提示:
1 <= nums.length <= 5000-104 <= nums[i] <= 104nums 中的每个值都 独一无二nums 在预先未知的某个下标上进行了旋转-104 <= target <= 104方法一:二分查找
我们使用二分,将数组分割成 [left, mid], [mid + 1, right] 两部分,这时候可以发现,其中有一部分一定是有序的。
因此,我们可以根据有序的那一部分,判断 target 是否在这一部分中:
[0, mid] 范围内的元素构成有序数组:
nums[0] <= target <= nums[mid],那么我们搜索范围可以缩小为 [left, mid];[mid + 1, right] 中查找;[mid + 1, n - 1] 范围内的元素构成有序数组:
nums[mid] < target <= nums[n - 1],那么我们搜索范围可以缩小为 [mid + 1, right];[left, mid] 中查找。二分查找终止条件是 left >= right,若结束后发现 nums[left] 与 target 不等,说明数组中不存在值为 target 的元素,返回 -1,否则返回下标 left。
class Solution { public int search(int[] nums, int target) { int n = nums.length; int left = 0, right = n - 1; while (left < right) { int mid = (left + right) >> 1; if (nums[0] <= nums[mid]) { if (nums[0] <= target && target <= nums[mid]) { right = mid; } else { left = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { left = mid + 1; } else { right = mid; } } } return nums[left] == target ? left : -1; } }
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
提示:
0 <= nums.length <= 105-109 <= nums[i] <= 109nums 是一个非递减数组-109 <= target <= 109方法一:二分查找
我们可以进行两次二分查找,分别查找出左边界和右边界。
时间复杂度 ,空间复杂度 。其中 是数组 nums 的长度。
以下是二分查找的两个通用模板:
模板 1:
boolean check(int x) {} int search(int left, int right) { while (left < right) { int mid = (left + right) >> 1; if (check(mid)) { right = mid; } else { left = mid + 1; } } return left; }
模板 2:
boolean check(int x) {} int search(int left, int right) { while (left < right) { int mid = (left + right + 1) >> 1; if (check(mid)) { left = mid; } else { right = mid - 1; } } return left; }
做二分题目时,可以按照以下步骤:
while (left < right),注意是 left < right,而非 left <= right;mid = (left + right) >> 1;check() 函数(有时很简单的逻辑,可以不定义 check),想一下究竟要用 right = mid(模板 1) 还是 left = mid(模板 2);
right = mid,那么无脑写出 else 语句 left = mid + 1,并且不需要更改 mid 的计算,即保持 mid = (left + right) >> 1;left = mid,那么无脑写出 else 语句 right = mid - 1,并且在 mid 计算时补充 +1,即 mid = (left + right + 1) >> 1。注意,这两个模板的优点是始终保持答案位于二分区间内,二分结束条件对应的值恰好在答案所处的位置。 对于可能无解的情况,只要判断二分结束后的 left 或者 right 是否满足题意即可。
class Solution { public int[] searchRange(int[] nums, int target) { int l = search(nums, target); int r = search(nums, target + 1); return l == r ? new int[] {-1, -1} : new int[] {l, r - 1}; } private int search(int[] nums, int x) { int left = 0, right = nums.length; while (left < right) { int mid = (left + right) >>> 1; if (nums[mid] >= x) { right = mid; } else { left = mid + 1; } } return left; } }
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
提示:
1 <= nums.length <= 104-104 <= nums[i] <= 104nums 为 无重复元素 的 升序 排列数组-104 <= target <= 104方法一:二分查找
由于 nums 数组已经有序,因此我们可以使用二分查找的方法找到目标值 target 的插入位置。
时间复杂度 ,空间复杂度 。其中 为数组 nums 的长度。
class Solution { public int searchInsert(int[] nums, int target) { int left = 0, right = nums.length; while (left < right) { int mid = (left + right) >>> 1; if (nums[mid] >= target) { right = mid; } else { left = mid + 1; } } return left; } }
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
1-9 在每一行只能出现一次。1-9 在每一列只能出现一次。1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)注意:
'.' 表示。示例 1:
输入:board = [["5","3",".",".","7",".",".",".","."] ,["6",".",".","1","9","5",".",".","."] ,[".","9","8",".",".",".",".","6","."] ,["8",".",".",".","6",".",".",".","3"] ,["4",".",".","8",".","3",".",".","1"] ,["7",".",".",".","2",".",".",".","6"] ,[".","6",".",".",".",".","2","8","."] ,[".",".",".","4","1","9",".",".","5"] ,[".",".",".",".","8",".",".","7","9"]] 输出:true
示例 2:
输入:board = [["8","3",".",".","7",".",".",".","."] ,["6",".",".","1","9","5",".",".","."] ,[".","9","8",".",".",".",".","6","."] ,["8",".",".",".","6",".",".",".","3"] ,["4",".",".","8",".","3",".",".","1"] ,["7",".",".",".","2",".",".",".","6"] ,[".","6",".",".",".",".","2","8","."] ,[".",".",".","4","1","9",".",".","5"] ,[".",".",".",".","8",".",".","7","9"]] 输出:false 解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
提示:
board.length == 9board[i].length == 9board[i][j] 是一位数字(1-9)或者 '.'方法一:一次遍历
有效的数独满足以下三个条件:
遍历数独,对于每个数字,判断其所在的行、列 以及 的宫格是否已经出现过该数字,如果是,则返回 false。遍历结束,返回 true。
时间复杂度 ,空间复杂度 ,其中 是数独中的空格数。本题中 。
class Solution { public boolean isValidSudoku(char[][] board) { boolean[][] row = new boolean[9][9]; boolean[][] col = new boolean[9][9]; boolean[][] sub = new boolean[9][9]; for (int i = 0; i < 9; ++i) { for (int j = 0; j < 9; ++j) { char c = board[i][j]; if (c == '.') { continue; } int num = c - '0' - 1; int k = i / 3 * 3 + j / 3; if (row[i][num] || col[j][num] || sub[k][num]) { return false; } row[i][num] = true; col[j][num] = true; sub[k][num] = true; } } return true; } }
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
1-9 在每一行只能出现一次。1-9 在每一列只能出现一次。1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)数独部分空格内已填入了数字,空白格用 '.' 表示。
示例 1:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] 输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]] 解释:输入的数独如上图所示,唯一有效的解决方案如下所示:![]()
提示:
board.length == 9board[i].length == 9board[i][j] 是一位数字或者 '.'方法一:回溯
我们用数组 row、col、box 分别记录每一行、每一列、每个 3x3 宫格中数字是否出现过。如果数字 i 在第 r 行、第 c 列、第 b 个 3x3 宫格中出现过,那么 row[r][i]、col[c][i]、box[b][i] 都为 true。
我们遍历 board 的每一个空格,枚举它可以填入的数字 v,如果 v 在当前行、当前列、当前 3x3 宫格中没有出现过,那么我们就可以尝试填入数字 v,并继续搜索下一个空格。如果搜索到最后,所有空格填充完毕,那么就说明找到了一个可行解。
时间复杂度 ,空间复杂度 。
class Solution { private boolean ok; private char[][] board; private List<Integer> t = new ArrayList<>(); private boolean[][] row = new boolean[9][9]; private boolean[][] col = new boolean[9][9]; private boolean[][][] block = new boolean[3][3][9]; public void solveSudoku(char[][] board) { this.board = board; for (int i = 0; i < 9; ++i) { for (int j = 0; j < 9; ++j) { if (board[i][j] == '.') { t.add(i * 9 + j); } else { int v = board[i][j] - '1'; row[i][v] = col[j][v] = block[i / 3][j / 3][v] = true; } } } dfs(0); } private void dfs(int k) { if (k == t.size()) { ok = true; return; } int i = t.get(k) / 9, j = t.get(k) % 9; for (int v = 0; v < 9; ++v) { if (!row[i][v] && !col[j][v] && !block[i / 3][j / 3][v]) { row[i][v] = col[j][v] = block[i / 3][j / 3][v] = true; board[i][j] = (char) (v + '1'); dfs(k + 1); row[i][v] = col[j][v] = block[i / 3][j / 3][v] = false; } if (ok) { return; } } } }
给定一个正整数 n ,输出外观数列的第 n 项。
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = "1"countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。前五项如下:
1. 1 2. 11 3. 21 4. 1211 5. 111221 第一项是数字 1 描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11" 描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21" 描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211" 描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
要 描述 一个数字字符串,首先要将字符串分割为 最小 数量的组,每个组都由连续的最多 相同字符 组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。
例如,数字字符串 "3322251" 的描述如下图:
示例 1:
输入:n = 1 输出:"1" 解释:这是一个基本样例。
示例 2:
输入:n = 4 输出:"1211" 解释: countAndSay(1) = "1" countAndSay(2) = 读 "1" = 一 个 1 = "11" countAndSay(3) = 读 "11" = 二 个 1 = "21" countAndSay(4) = 读 "21" = 一 个 2 + 一 个 1 = "12" + "11" = "1211"
提示:
1 <= n <= 30class Solution { public String countAndSay(int n) { String s = "1"; while (--n > 0) { StringBuilder t = new StringBuilder(); for (int i = 0; i < s.length();) { int j = i; while (j < s.length() && s.charAt(j) == s.charAt(i)) { ++j; } t.append((j - i) + ""); t.append(s.charAt(i)); i = j; } s = t.toString(); } return s; } }
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1 输出: []
提示:
1 <= candidates.length <= 302 <= candidates[i] <= 40candidates 的所有元素 互不相同1 <= target <= 40方法一:排序 + 剪枝 + 回溯(两种写法)
我们可以先对数组进行排序,方便剪枝。
接下来,我们设计一个函数 ,表示从下标 开始搜索,且剩余目标值为 ,其中 和 都是非负整数,当前搜索路径为 ,答案为 。
在函数 中,我们先判断 是否为 ,如果是,则将当前搜索路径 加入答案 中,然后返回。如果 ,说明当前下标及后面的下标的元素都大于剩余目标值 ,路径不合法,直接返回。否则,我们从下标 开始搜索,搜索的下标范围是 ,其中 为数组 的长度。在搜索的过程中,我们将当前下标的元素加入搜索路径 ,递归调用函数 ,递归结束后,将当前下标的元素从搜索路径 中移除。
我们也可以将函数 的实现逻辑改为另一种写法。在函数 中,我们先判断 是否为 ,如果是,则将当前搜索路径 加入答案 中,然后返回。如果 或者 ,路径不合法,直接返回。否则,我们考虑两种情况,一种是不选当前下标的元素,即递归调用函数 ,另一种是选当前下标的元素,即递归调用函数 。
在主函数中,我们只要调用函数 ,即可得到答案。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。由于剪枝,实际的时间复杂度要远小于 。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int[] candidates; public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates); this.candidates = candidates; dfs(0, target); return ans; } private void dfs(int i, int s) { if (s == 0) { ans.add(new ArrayList(t)); return; } if (s < candidates[i]) { return; } for (int j = i; j < candidates.length; ++j) { t.add(candidates[j]); dfs(j, s - candidates[j]); t.remove(t.size() - 1); } } }
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int[] candidates; public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates); this.candidates = candidates; dfs(0, target); return ans; } private void dfs(int i, int s) { if (s == 0) { ans.add(new ArrayList(t)); return; } if (i >= candidates.length || s < candidates[i]) { return; } dfs(i + 1, s); t.add(candidates[i]); dfs(i, s - candidates[i]); t.remove(t.size() - 1); } }
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]
提示:
1 <= candidates.length <= 1001 <= candidates[i] <= 501 <= target <= 30方法一:排序 + 剪枝 + 回溯(两种写法)
我们可以先对数组进行排序,方便剪枝以及跳过重复的数字。
接下来,我们设计一个函数 ,表示从下标 开始搜索,且剩余目标值为 ,其中 和 都是非负整数,当前搜索路径为 ,答案为 。
在函数 中,我们先判断 是否为 ,如果是,则将当前搜索路径 加入答案 中,然后返回。如果 ,或者 ,说明当前路径不合法,直接返回。否则,我们从下标 开始搜索,搜索的下标范围是 ,其中 为数组 的长度。在搜索的过程中,如果 并且 ,说明当前数字与上一个数字相同,我们可以跳过当前数字,因为上一个数字已经搜索过了。否则,我们将当前数字加入搜索路径 中,然后递归调用函数 ,然后将当前数字从搜索路径 中移除。
我们也可以将函数 的实现逻辑改为另一种写法。如果我们选择当前数字,那么我们将当前数字加入搜索路径 中,然后递归调用函数 ,然后将当前数字从搜索路径 中移除。如果我们不选择当前数字,那么我们可以跳过与当前数字相同的所有数字,然后递归调用函数 ,其中 为第一个与当前数字不同的数字的下标。
在主函数中,我们只要调用函数 ,即可得到答案。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。由于剪枝,实际的时间复杂度要远小于 。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int[] candidates; public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); this.candidates = candidates; dfs(0, target); return ans; } private void dfs(int i, int s) { if (s == 0) { ans.add(new ArrayList<>(t)); return; } if (i >= candidates.length || s < candidates[i]) { return; } for (int j = i; j < candidates.length; ++j) { if (j > i && candidates[j] == candidates[j - 1]) { continue; } t.add(candidates[j]); dfs(j + 1, s - candidates[j]); t.remove(t.size() - 1); } } }
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int[] candidates; public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); this.candidates = candidates; dfs(0, target); return ans; } private void dfs(int i, int s) { if (s == 0) { ans.add(new ArrayList<>(t)); return; } if (i >= candidates.length || s < candidates[i]) { return; } int x = candidates[i]; t.add(x); dfs(i + 1, s - x); t.remove(t.size() - 1); while (i < candidates.length && candidates[i] == x) { ++i; } dfs(i, s); } }
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0] 输出:3
示例 2:
输入:nums = [3,4,-1,1] 输出:2
示例 3:
输入:nums = [7,8,9,11,12] 输出:1
提示:
1 <= nums.length <= 5 * 105-231 <= nums[i] <= 231 - 1方法一:原地交换
我们假设数组 nums 长度为 ,那么最小的正整数一定在 之间。我们可以遍历数组,将数组中的每个数 交换到它应该在的位置上,即 应该在的位置为 。如果 不在 之间,那么我们就不用管它。
遍历结束后,我们再遍历数组,如果 不等于 ,那么 就是我们要找的最小的正整数。
时间复杂度 ,空间复杂度 。
class Solution { public int firstMissingPositive(int[] nums) { int n = nums.length; for (int i = 0; i < n; ++i) { while (nums[i] >= 1 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) { swap(nums, i, nums[i] - 1); } } for (int i = 0; i < n; ++i) { if (i + 1 != nums[i]) { return i + 1; } } return n + 1; } private void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } }
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5] 输出:9
提示:
n == height.length1 <= n <= 2 * 1040 <= height[i] <= 105方法一:动态规划
我们定义 表示下标 位置及其左边的最高柱子的高度,定义 表示下标 位置及其右边的最高柱子的高度。那么下标 位置能接的雨水量为 。我们遍历数组,计算出 和 ,最后答案为 。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { public int trap(int[] height) { int n = height.length; int[] left = new int[n]; int[] right = new int[n]; left[0] = height[0]; right[n - 1] = height[n - 1]; for (int i = 1; i < n; ++i) { left[i] = Math.max(left[i - 1], height[i]); right[n - i - 1] = Math.max(right[n - i], height[n - i - 1]); } int ans = 0; for (int i = 0; i < n; ++i) { ans += Math.min(left[i], right[i]) - height[i]; } return ans; } }
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
示例 1:
输入: num1 = "2", num2 = "3" 输出: "6"
示例 2:
输入: num1 = "123", num2 = "456" 输出: "56088"
提示:
1 <= num1.length, num2.length <= 200num1 和 num2 只能由数字组成。num1 和 num2 都不包含任何前导零,除了数字0本身。方法一:数学乘法模拟
假设 num1 和 num2 的长度分别为 和 ,则它们的乘积的长度最多为 。
证明如下:
num1 和 num2 都取最小值,那么它们的乘积为 ,长度为 。num1 和 num2 都取最大值,那么它们的乘积为 ,长度为 。因此,我们可以申请一个长度为 的数组,用于存储乘积的每一位。
从低位到高位,依次计算乘积的每一位,最后将数组转换为字符串即可。
注意判断最高位是否为 ,如果是,则去掉。
时间复杂度 ,空间复杂度 。其中 和 分别为 num1 和 num2 的长度。
class Solution { public String multiply(String num1, String num2) { if ("0".equals(num1) || "0".equals(num2)) { return "0"; } int m = num1.length(), n = num2.length(); int[] arr = new int[m + n]; for (int i = m - 1; i >= 0; --i) { int a = num1.charAt(i) - '0'; for (int j = n - 1; j >= 0; --j) { int b = num2.charAt(j) - '0'; arr[i + j + 1] += a * b; } } for (int i = arr.length - 1; i > 0; --i) { arr[i - 1] += arr[i] / 10; arr[i] %= 10; } int i = arr[0] == 0 ? 1 : 0; StringBuilder ans = new StringBuilder(); for (; i < arr.length; ++i) { ans.append(arr[i]); } return ans.toString(); } }
s) 和一个字符模式 (p) ,请你实现一个支持 '?' 和 '*' 匹配规则的通配符匹配:'?' 可以匹配任何单个字符。'*' 可以匹配任意字符序列(包括空字符序列)。判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
示例 1:
输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "*" 输出:true 解释:'*' 可以匹配任意字符串。
示例 3:
输入:s = "cb", p = "?a" 输出:false 解释:'?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
提示:
0 <= s.length, p.length <= 2000s 仅由小写英文字母组成p 仅由小写英文字母、'?' 或 '*' 组成方法一:动态规划
定义状态 表示 的前 个字符和 的前 个字符是否匹配。
状态转移方程如下:
时间复杂度 ,空间复杂度 。
class Solution { public boolean isMatch(String s, String p) { int m = s.length(), n = p.length(); boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; for (int j = 1; j <= n; ++j) { if (p.charAt(j - 1) == '*') { dp[0][j] = dp[0][j - 1]; } } for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '?') { dp[i][j] = dp[i - 1][j - 1]; } else if (p.charAt(j - 1) == '*') { dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; } } } return dp[m][n]; } }
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i] i + j < n返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例 1:
输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是 2。 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4] 输出: 2
提示:
1 <= nums.length <= 1040 <= nums[i] <= 1000nums[n-1]方法一:贪心
我们可以用变量 记录当前位置能够到达的最远位置,用变量 记录上一次跳跃到的位置,用变量 记录跳跃的次数。
接下来,我们遍历 的每一个位置 ,对于每一个位置 ,我们可以通过 计算出当前位置能够到达的最远位置,我们用 来记录这个最远位置,即 。接下来,判断当前位置是否到达了上一次跳跃的边界,即 ,如果到达了,那么我们就需要进行一次跳跃,将 更新为 ,并且将跳跃次数 增加 。
最后,我们返回跳跃的次数 即可。
时间复杂度 ,其中 是数组的长度。空间复杂度 。
相似题目:
class Solution { public int jump(int[] nums) { int ans = 0, mx = 0, last = 0; for (int i = 0; i < nums.length - 1; ++i) { mx = Math.max(mx, i + nums[i]); if (last == i) { ++ans; last = mx; } } return ans; } }
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1] 输出:[[1]]
提示:
1 <= nums.length <= 6-10 <= nums[i] <= 10nums 中的所有整数 互不相同方法一:DFS(回溯)
我们设计一个函数 表示已经填完了前 个位置,现在需要填第 个位置。枚举所有可能的数,如果这个数没有被填过,就填入这个数,然后继续填下一个位置,直到填完所有的位置。
时间复杂度 ,其中 是数组的长度。一共有 个排列,每个排列需要 的时间来构造。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private boolean[] vis; private int[] nums; public List<List<Integer>> permute(int[] nums) { this.nums = nums; vis = new boolean[nums.length]; dfs(0); return ans; } private void dfs(int i) { if (i == nums.length) { ans.add(new ArrayList<>(t)); return; } for (int j = 0; j < nums.length; ++j) { if (!vis[j]) { vis[j] = true; t.add(nums[j]); dfs(i + 1); t.remove(t.size() - 1); vis[j] = false; } } } }
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8-10 <= nums[i] <= 10方法一:排序 + 回溯
我们可以先对数组进行排序,这样就可以将重复的数字放在一起,方便我们进行去重。
然后,我们设计一个函数 ,表示当前需要填写第 个位置的数。函数的具体实现如下:
在主函数中,我们首先对数组进行排序,然后调用 ,即从第 0 个位置开始填写,最终返回答案数组即可。
时间复杂度 ,空间复杂度 。其中 是数组的长度。需要进行 次枚举,每次枚举需要 的时间来判断是否重复。另外,我们需要一个标记数组来标记每个位置是否被使用过,因此空间复杂度为 。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int[] nums; private boolean[] vis; public List<List<Integer>> permuteUnique(int[] nums) { Arrays.sort(nums); this.nums = nums; vis = new boolean[nums.length]; dfs(0); return ans; } private void dfs(int i) { if (i == nums.length) { ans.add(new ArrayList<>(t)); return; } for (int j = 0; j < nums.length; ++j) { if (vis[j] || (j > 0 && nums[j] == nums[j - 1] && !vis[j - 1])) { continue; } t.add(nums[j]); vis[j] = true; dfs(i + 1); vis[j] = false; t.remove(t.size() - 1); } } }
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
提示:
n == matrix.length == matrix[i].length1 <= n <= 20-1000 <= matrix[i][j] <= 1000方法一:原地翻转
根据题目要求,我们实际上需要将 旋转至 。
我们可以先对矩阵进行上下翻转,即 和 进行交换,然后再对矩阵进行主对角线翻转,即 和 进行交换。这样就能将 旋转至 了。
时间复杂度 ,其中 是矩阵的边长。空间复杂度 。
class Solution { public void rotate(int[][] matrix) { int n = matrix.length; for (int i = 0; i<n> > 1; ++i) { for (int j = 0; j < n; ++j) { int t = matrix[i][j]; matrix[i][j] = matrix[n - i - 1][j]; matrix[n - i - 1][j] = t; } } for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { int t = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = t; } } } }
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""] 输出: [[""]]
示例 3:
输入: strs = ["a"] 输出: [["a"]]
提示:
1 <= strs.length <= 1040 <= strs[i].length <= 100strs[i] 仅包含小写字母方法一:哈希表
key,[str] 为 value,存入哈希表当中(HashMap<String, List<String>>)。key 时,将其加入到对应的 value 当中即可。以 strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 为例,遍历结束时,哈希表的状况:
| key | value |
|---|---|
"aet" |
["eat", "tea", "ate"] |
"ant" |
["tan", "nat"] |
"abt" |
["bat"] |
最后返回哈希表的 value 列表即可。
时间复杂度 。其中 和 分别是字符串数组的长度和字符串的最大长度。
方法二:计数
我们也可以将方法一中的排序部分改为计数,也就是说,将每个字符串 中的字符以及出现的次数作为 key,将字符串 作为 value 存入哈希表当中。
时间复杂度 。其中 和 分别是字符串数组的长度和字符串的最大长度,而 是字符集的大小,本题中 。
class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String, List<String>> d = new HashMap<>(); for (String s : strs) { char[] t = s.toCharArray(); Arrays.sort(t); String k = String.valueOf(t); d.computeIfAbsent(k, key -> new ArrayList<>()).add(s); } return new ArrayList<>(d.values()); } }
class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String, List<String>> d = new HashMap<>(); for (String s : strs) { int[] cnt = new int[26]; for (int i = 0; i < s.length(); ++i) { ++cnt[s.charAt(i) - 'a']; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < 26; ++i) { if (cnt[i] > 0) { sb.append((char) ('a' + i)).append(cnt[i]); } } String k = sb.toString(); d.computeIfAbsent(k, key -> new ArrayList<>()).add(s); } return new ArrayList<>(d.values()); } }
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn )。
示例 1:
输入:x = 2.00000, n = 10 输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3 输出:9.26100
示例 3:
输入:x = 2.00000, n = -2 输出:0.25000 解释:2-2 = 1/22 = 1/4 = 0.25
提示:
-100.0 < x < 100.0-231 <= n <= 231-1n 是一个整数-104 <= xn <= 104方法一:数学(快速幂)
快速幂算法的核心思想是将幂指数 拆分为若干个二进制位上的 的和,然后将 的 次幂转化为 的若干个幂的乘积。
时间复杂度 ,空间复杂度 。其中 为幂指数。
class Solution { public double myPow(double x, int n) { long N = n; return n >= 0 ? qmi(x, N) : 1.0 / qmi(x, -N); } private double qmi(double a, long k) { double res = 1; while (k != 0) { if ((k & 1) != 0) { res *= a; } a *= a; k >>= 1; } return res; } }
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:
输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1 输出:[["Q"]]
提示:
1 <= n <= 9深度优先搜索 + 剪枝。
class Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> res = new ArrayList<>(); String[][] g = new String[n][n]; for (int i = 0; i < n; ++i) { String[] t = new String[n]; Arrays.fill(t, "."); g[i] = t; } // 列是否已经有值 boolean[] col = new boolean[n]; // 斜线是否已经有值 boolean[] dg = new boolean[2 * n]; // 反斜线是否已经有值 boolean[] udg = new boolean[2 * n]; // 从第一行开始搜索 dfs(0, n, col, dg, udg, g, res); return res; } private void dfs(int u, int n, boolean[] col, boolean[] dg, boolean[] udg, String[][] g, List<List<String>> res) { if (u == n) { List<String> t = new ArrayList<>(); for (String[] e : g) { t.add(String.join("", e)); } res.add(t); return; } for (int i = 0; i < n; ++i) { if (!col[i] && !dg[u + i] && !udg[n - u + i]) { g[u][i] = "Q"; col[i] = dg[u + i] = udg[n - u + i] = true; dfs(u + 1, n, col, dg, udg, g, res); g[u][i] = "."; col[i] = dg[u + i] = udg[n - u + i] = false; } } } }
n 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。
示例 1:
输入:n = 4 输出:2 解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1 输出:1
提示:
1 <= n <= 9方法一:回溯
我们设计一个函数 ,表示从第 行开始搜索,搜索到的结果累加到答案中。
在第 行,我们枚举第 行的每一列,如果当前列不与前面已经放置的皇后发生冲突,那么我们就可以放置一个皇后,然后继续搜索下一行,即调用 。
如果发生冲突,那么我们就跳过当前列,继续枚举下一列。
判断是否发生冲突,我们需要用三个数组分别记录每一列、每一条正对角线、每一条反对角线是否已经放置了皇后。
具体地,我们用 数组记录每一列是否已经放置了皇后,用 数组记录每一条正对角线是否已经放置了皇后,用 数组记录每一条反对角线是否已经放置了皇后。
时间复杂度 ,空间复杂度 。其中 是皇后的数量。
class Solution { private int n; private int ans; private boolean[] cols = new boolean[10]; private boolean[] dg = new boolean[20]; private boolean[] udg = new boolean[20]; public int totalNQueens(int n) { this.n = n; dfs(0); return ans; } private void dfs(int i) { if (i == n) { ++ans; return; } for (int j = 0; j < n; ++j) { int a = i + j, b = i - j + n; if (cols[j] || dg[a] || udg[b]) { continue; } cols[j] = true; dg[a] = true; udg[b] = true; dfs(i + 1); cols[j] = false; dg[a] = false; udg[b] = false; } } }
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1] 输出:1
示例 3:
输入:nums = [5,4,-1,7,8] 输出:23
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 104进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
设 dp[i] 表示 [0..i] 中,以 nums[i] 结尾的最大子数组和,状态转移方程 dp[i] = nums[i] + max(dp[i - 1], 0)。
由于 dp[i] 只与子问题 dp[i-1] 有关,故可以用一个变量 f 来表示。
最大子数组和可能有三种情况:
递归求得三者,返回最大值即可。
动态规划:
分治:
动态规划:
class Solution { public int maxSubArray(int[] nums) { int f = nums[0], res = nums[0]; for (int i = 1, n = nums.length; i < n; ++i) { f = nums[i] + Math.max(f, 0); res = Math.max(res, f); } return res; } }
分治:
class Solution { public int maxSubArray(int[] nums) { return maxSub(nums, 0, nums.length - 1); } private int maxSub(int[] nums, int left, int right) { if (left == right) { return nums[left]; } int mid = (left + right) >>> 1; int lsum = maxSub(nums, left, mid); int rsum = maxSub(nums, mid + 1, right); return Math.max(Math.max(lsum, rsum), crossMaxSub(nums, left, mid, right)); } private int crossMaxSub(int[] nums, int left, int mid, int right) { int lsum = 0, rsum = 0; int lmx = Integer.MIN_VALUE, rmx = Integer.MIN_VALUE; for (int i = mid; i >= left; --i) { lsum += nums[i]; lmx = Math.max(lmx, lsum); } for (int i = mid + 1; i <= right; ++i) { rsum += nums[i]; rmx = Math.max(rmx, rsum); } return lmx + rmx; } }
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 输出:[1,2,3,4,8,12,11,10,9,5,6,7]
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100方法一:模拟
我们用 和 分别表示当前访问到的元素的行和列,用 表示当前的方向,用数组或哈希表 记录每个元素是否被访问过。每次我们访问到一个元素后,将其标记为已访问,然后按照当前的方向前进一步,如果前进一步后发现越界或者已经访问过,则改变方向继续前进,直到遍历完整个矩阵。
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
对于访问过的元素,我们也可以将其值加上一个常数 ,这样就不需要额外的 数组或哈希表来记录是否访问过了,从而将空间复杂度降低到 。
方法二:逐层模拟
我们也可以从外往里一圈一圈遍历并存储矩阵元素。
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
class Solution { public List<Integer> spiralOrder(int[][] matrix) { int m = matrix.length, n = matrix[0].length; int[] dirs = {0, 1, 0, -1, 0}; int i = 0, j = 0, k = 0; List<Integer> ans = new ArrayList<>(); boolean[][] vis = new boolean[m][n]; for (int h = m * n; h > 0; --h) { ans.add(matrix[i][j]); vis[i][j] = true; int x = i + dirs[k], y = j + dirs[k + 1]; if (x < 0 || x >= m || y < 0 || y >= n || vis[x][y]) { k = (k + 1) % 4; } i += dirs[k]; j += dirs[k + 1]; } return ans; } }
class Solution { public List<Integer> spiralOrder(int[][] matrix) { int m = matrix.length, n = matrix[0].length; int[] dirs = {0, 1, 0, -1, 0}; List<Integer> ans = new ArrayList<>(); for (int h = m * n, i = 0, j = 0, k = 0; h > 0; --h) { ans.add(matrix[i][j]); matrix[i][j] += 300; int x = i + dirs[k], y = j + dirs[k + 1]; if (x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] > 100) { k = (k + 1) % 4; } i += dirs[k]; j += dirs[k + 1]; } // for (int i = 0; i < m; ++i) { // for (int j = 0; j < n; ++j) { // matrix[i][j] -= 300; // } // } return ans; } }
class Solution { public List<Integer> spiralOrder(int[][] matrix) { int m = matrix.length, n = matrix[0].length; int x1 = 0, y1 = 0, x2 = m - 1, y2 = n - 1; List<Integer> ans = new ArrayList<>(); while (x1 <= x2 && y1 <= y2) { for (int j = y1; j <= y2; ++j) { ans.add(matrix[x1][j]); } for (int i = x1 + 1; i <= x2; ++i) { ans.add(matrix[i][y2]); } if (x1 < x2 && y1 < y2) { for (int j = y2 - 1; j >= y1; --j) { ans.add(matrix[x2][j]); } for (int i = x2 - 1; i > x1; --i) { ans.add(matrix[i][y1]); } } ++x1; ++y1; --x2; --y2; } return ans; } }
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 3 * 1040 <= nums[i] <= 105方法一:贪心
我们用变量 维护当前能够到达的最远下标,初始时 。
我们从左到右遍历数组,对于遍历到的每个位置 ,如果 ,说明当前位置无法到达,直接返回 false。否则,我们可以通过跳跃从位置 到达的最远位置为 ,我们用 更新 的值,即 。
遍历结束,直接返回 true。
时间复杂度 ,其中 为数组的长度。空间复杂度 。
相似题目:
class Solution { public boolean canJump(int[] nums) { int mx = 0; for (int i = 0; i < nums.length; ++i) { if (mx < i) { return false; } mx = Math.max(mx, i + nums[i]); } return true; } }
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 104intervals[i].length == 20 <= starti <= endi <= 104方法一:区间合并
这道题是一道典型的区间合并问题,即给定一些区间,要求将所有有交集的区间合并成一个区间。
我们可以将区间按照左端点升序排列,然后遍历区间进行合并操作。
模板如下:
时间复杂度 ,空间复杂度 。其中 为区间个数。
class Solution { public int[][] merge(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); int st = intervals[0][0], ed = intervals[0][1]; List<int[]> ans = new ArrayList<>(); for (int i = 1; i < intervals.length; ++i) { int s = intervals[i][0], e = intervals[i][1]; if (ed < s) { ans.add(new int[] {st, ed}); st = s; ed = e; } else { ed = Math.max(ed, e); } } ans.add(new int[] {st, ed}); return ans.toArray(new int[ans.size()][]); } }
给你一个 无重叠的 ,按照区间起始端点排序的区间列表。
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
示例 1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5] 输出:[[1,5],[6,9]]
示例 2:
输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] 输出:[[1,2],[3,10],[12,16]] 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
示例 3:
输入:intervals = [], newInterval = [5,7] 输出:[[5,7]]
示例 4:
输入:intervals = [[1,5]], newInterval = [2,3] 输出:[[1,5]]
示例 5:
输入:intervals = [[1,5]], newInterval = [2,7] 输出:[[1,7]]
提示:
0 <= intervals.length <= 104intervals[i].length == 20 <= intervals[i][0] <= intervals[i][1] <= 105intervals 根据 intervals[i][0] 按 升序 排列newInterval.length == 20 <= newInterval[0] <= newInterval[1] <= 105方法一:排序 + 区间合并
我们可以先将新区间 newInterval 加入到区间列表 intervals 中,然后按照区间合并的常规方法进行合并。
时间复杂度 ,空间复杂度 。其中 是区间的数量。
方法二:一次遍历
我们可以遍历区间列表 intervals,记当前区间为 interval,对于每个区间有三种情况:
遍历结束,如果新区间还没有被加入,那么将新区间加入到答案中。
时间复杂度 ,空间复杂度 。其中 是区间的数量。
class Solution { public int[][] insert(int[][] intervals, int[] newInterval) { List<int[]> ans = new ArrayList<>(); int st = newInterval[0], ed = newInterval[1]; boolean insert = false; for (int[] interval : intervals) { int s = interval[0], e = interval[1]; if (ed < s) { if (!insert) { ans.add(new int[] {st, ed}); insert = true; } ans.add(interval); } else if (e < st) { ans.add(interval); } else { st = Math.min(st, s); ed = Math.max(ed, e); } } if (!insert) { ans.add(new int[] {st, ed}); } return ans.toArray(new int[ans.size()][]); } }
给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。
单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。
示例 1:
输入:s = "Hello World" 输出:5 解释:最后一个单词是“World”,长度为5。
示例 2:
输入:s = " fly me to the moon " 输出:4 解释:最后一个单词是“moon”,长度为4。
示例 3:
输入:s = "luffy is still joyboy" 输出:6 解释:最后一个单词是长度为6的“joyboy”。
提示:
1 <= s.length <= 104s 仅有英文字母和空格 ' ' 组成s 中至少存在一个单词方法一:逆向遍历 + 双指针
我们从字符串 末尾开始遍历,找到第一个不为空格的字符,即为最后一个单词的最后一个字符,下标记为 。然后继续向前遍历,找到第一个为空格的字符,即为最后一个单词的第一个字符的前一个字符,记为 。那么最后一个单词的长度即为 。
时间复杂度 ,其中 为字符串 长度。空间复杂度 。
class Solution { public int lengthOfLastWord(String s) { int i = s.length() - 1; while (i >= 0 && s.charAt(i) == ' ') { --i; } int j = i; while (j >= 0 && s.charAt(j) != ' ') { --j; } return i - j; } }
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1 输出:[[1]]
提示:
1 <= n <= 20方法一:模拟
直接模拟螺旋矩阵的生成过程。
定义一个二维数组 ans,用于存储螺旋矩阵。用 i 和 j 分别表示当前位置的行号和列号,用 k 表示当前的方向编号,dirs 表示方向编号与方向的对应关系。
从 1 开始,依次填入矩阵中的每个位置。每次填入一个位置后,计算下一个位置的行号和列号,如果下一个位置不在矩阵中或者已经被填过,则改变方向,再计算下一个位置的行号和列号。
时间复杂度 ,其中 是矩阵的边长。忽略输出数组不计,空间复杂度 。
class Solution { public int[][] generateMatrix(int n) { int[][] ans = new int[n][n]; int i = 0, j = 0, k = 0; int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; for (int v = 1; v <= n * n; ++v) { ans[i][j] = v; int x = i + dirs[k][0], y = j + dirs[k][1]; if (x < 0 || y < 0 || x >= n || y >= n || ans[x][y] > 0) { k = (k + 1) % 4; x = i + dirs[k][0]; y = j + dirs[k][1]; } i = x; j = y; } return ans; } }
给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
"123""132""213""231""312""321"给定 n 和 k,返回第 k 个排列。
示例 1:
输入:n = 3, k = 3 输出:"213"
示例 2:
输入:n = 4, k = 9 输出:"2314"
示例 3:
输入:n = 3, k = 1 输出:"123"
提示:
1 <= n <= 91 <= k <= n!方法一:枚举
我们知道,集合 一共有 种排列,如果我们确定首位,那剩余位能组成的排列数量为 。
因此,我们枚举每一位 ,如果此时 大于当前位置确定后的排列数量,那么我们可以直接减去这个数量;否则,说明我们找到了当前位置的数。
对于每一位 ,其中 ,剩余位能组成的排列数量为 ,我们记为 。过程中已使用的数记录在 vis 中。
时间复杂度 ,空间复杂度 。
class Solution { public String getPermutation(int n, int k) { StringBuilder ans = new StringBuilder(); boolean[] vis = new boolean[n + 1]; for (int i = 0; i < n; ++i) { int fact = 1; for (int j = 1; j < n - i; ++j) { fact *= j; } for (int j = 1; j <= n; ++j) { if (!vis[j]) { if (k > fact) { k -= fact; } else { ans.append(j); vis[j] = true; break; } } } } return ans.toString(); } }
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
示例 2:
输入:head = [0,1,2], k = 4 输出:[2,0,1]
提示:
[0, 500] 内-100 <= Node.val <= 1000 <= k <= 2 * 109将链表右半部分的 k 的节点拼接到 head 即可。
注:k 对链表长度 n 取余,即 k %= n。
class Solution { public ListNode rotateRight(ListNode head, int k) { if (k == 0 || head == null || head.next == null) { return head; } int n = 0; for (ListNode cur = head; cur != null; cur = cur.next) { ++n; } k %= n; if (k == 0) { return head; } ListNode slow = head, fast = head; while (k-- > 0) { fast = fast.next; } while (fast.next != null) { slow = slow.next; fast = fast.next; } ListNode start = slow.next; slow.next = null; fast.next = head; return start; } }
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28
示例 2:
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3 输出:28
示例 4:
输入:m = 3, n = 3 输出:6
提示:
1 <= m, n <= 1002 * 109方法一:动态规划
假设 dp[i][j] 表示到达网格 (i, j) 的路径数,则 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
class Solution { public int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; for (int i = 0; i < m; ++i) { Arrays.fill(dp[i], 1); } for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; } }
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]] 输出:1
提示:
m == obstacleGrid.lengthn == obstacleGrid[i].length1 <= m, n <= 100obstacleGrid[i][j] 为 0 或 1动态规划。
假设 dp[i][j] 表示到达网格 (i,j) 的路径数,先初始化 dp 第一列和第一行的所有值,然后判断。
obstacleGrid[i][j] == 1,说明路径数为 0,dp[i][j] = 0;obstacleGrid[i][j] == 0,则 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。最后返回 dp[m - 1][n - 1] 即可。
class Solution { public int uniquePathsWithObstacles(int[][] obstacleGrid) { int m = obstacleGrid.length, n = obstacleGrid[0].length; int[][] dp = new int[m][n]; for (int i = 0; i < m && obstacleGrid[i][0] == 0; ++i) { dp[i][0] = 1; } for (int j = 0; j < n && obstacleGrid[0][j] == 0; ++j) { dp[0][j] = 1; } for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { if (obstacleGrid[i][j] == 0) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } } return dp[m - 1][n - 1]; } }
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]] 输出:12
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 100方法一:动态规划
假设 dp[i][j] 表示到达网格 (i,j) 的最小数字和,先初始化 dp 第一列和第一行的所有值,然后利用递推公式:dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j] 求得 dp。
最后返回 dp[m - 1][n - 1] 即可。
class Solution { public int minPathSum(int[][] grid) { int m = grid.length, n = grid[0].length; int[][] dp = new int[m][n]; dp[0][0] = grid[0][0]; for (int i = 1; i < m; ++i) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } for (int j = 1; j < n; ++j) { dp[0][j] = dp[0][j - 1] + grid[0][j]; } for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } return dp[m - 1][n - 1]; } }
有效数字(按顺序)可以分成以下几个部分:
'e' 或 'E' ,后面跟着一个 整数小数(按顺序)可以分成以下几个部分:
'+' 或 '-')'.''.' ,后面再跟着至少一位数字'.' ,后面跟着至少一位数字整数(按顺序)可以分成以下几个部分:
'+' 或 '-')部分有效数字列举如下:["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"]
部分无效数字列举如下:["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]
给你一个字符串 s ,如果 s 是一个 有效数字 ,请返回 true 。
示例 1:
输入:s = "0" 输出:true
示例 2:
输入:s = "e" 输出:false
示例 3:
输入:s = "." 输出:false
提示:
1 <= s.length <= 20s 仅含英文字母(大写和小写),数字(0-9),加号 '+' ,减号 '-' ,或者点 '.' 。方法一:分情况讨论
首先,我们判断字符串是否以正负号开头,如果是,将指针 向后移动一位。如果此时指针 已经到达字符串末尾,说明字符串只有一个正负号,返回 false。
如果当前指针 指向的字符是小数点,并且小数点后面没有数字,或者小数点后是一个 e 或 E,返回 false。
接着,我们用两个变量 和 分别记录小数点和 e 或 E 的个数。
用指针 指向当前字符:
e 或 E,返回 false。否则,我们将 加一;e 或 E,并且此前出现过 e 或 E,或者当前字符是开头或结尾,返回 false。否则,我们将 加一;然后判断下一个字符是否是正负号,如果是,将指针 向后移动一位。如果此时指针 已经到达字符串末尾,返回 false;false。遍历完字符串后,返回 true。
时间复杂度 ,空间复杂度 。其中 为字符串长度。
class Solution { public boolean isNumber(String s) { int n = s.length(); int i = 0; if (s.charAt(i) == '+' || s.charAt(i) == '-') { ++i; } if (i == n) { return false; } if (s.charAt(i) == '.' && (i + 1 == n || s.charAt(i + 1) == 'e' || s.charAt(i + 1) == 'E')) { return false; } int dot = 0, e = 0; for (int j = i; j < n; ++j) { if (s.charAt(j) == '.') { if (e > 0 || dot > 0) { return false; } ++dot; } else if (s.charAt(j) == 'e' || s.charAt(j) == 'E') { if (e > 0 || j == i || j == n - 1) { return false; } ++e; if (s.charAt(j + 1) == '+' || s.charAt(j + 1) == '-') { if (++j == n - 1) { return false; } } } else if (s.charAt(j) < '0' || s.charAt(j) > '9') { return false; } } return true; } }
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入:digits = [1,2,3] 输出:[1,2,4] 解释:输入数组表示数字 123。
示例 2:
输入:digits = [4,3,2,1] 输出:[4,3,2,2] 解释:输入数组表示数字 4321。
示例 3:
输入:digits = [0] 输出:[1]
提示:
1 <= digits.length <= 1000 <= digits[i] <= 9class Solution { public int[] plusOne(int[] digits) { int n = digits.length; for (int i = n - 1; i >= 0; --i) { ++digits[i]; digits[i] %= 10; if (digits[i] != 0) { return digits; } } digits = new int[n + 1]; digits[0] = 1; return digits; } }
给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。
示例 1:
输入:a = "11", b = "1" 输出:"100"
示例 2:
输入:a = "1010", b = "1011" 输出:"10101"
提示:
1 <= a.length, b.length <= 104a 和 b 仅由字符 '0' 或 '1' 组成"0" ,就不含前导零class Solution { public String addBinary(String a, String b) { StringBuilder sb = new StringBuilder(); for (int i = a.length() - 1, j = b.length() - 1, carry = 0; i >= 0 || j >= 0 || carry > 0; --i, --j) { carry += (i >= 0 ? a.charAt(i) - '0' : 0) + (j >= 0 ? b.charAt(j) - '0' : 0); sb.append(carry % 2); carry /= 2; } return sb.reverse().toString(); } }
给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。
你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 maxWidth 个字符。
要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。
文本的最后一行应为左对齐,且单词之间不插入额外的空格。
注意:
words 至少包含一个单词。示例 1:
输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16 输出: [ "This is an", "example of text", "justification. " ]
示例 2:
输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16
输出:
[
"What must be",
"acknowledgment ",
"shall be "
]
解释: 注意最后一行的格式应为 "shall be " 而不是 "shall be",
因为最后一行应为左对齐,而不是左右两端对齐。
第二行同样为左对齐,这是因为这行只包含一个单词。
示例 3:
输入:words = ["Science","is","what","we","understand","well","enough","to","explain","to","a","computer.","Art","is","everything","else","we","do"],maxWidth = 20 输出: [ "Science is what we", "understand well", "enough to explain to", "a computer. Art is", "everything else we", "do " ]
提示:
1 <= words.length <= 3001 <= words[i].length <= 20words[i] 由小写英文字母和符号组成1 <= maxWidth <= 100words[i].length <= maxWidth方法一:模拟
根据题意模拟即可,注意,如果是最后一行,或者这一行只有一个单词,那么要左对齐,否则要均匀分配空格。
时间复杂度 ,空间复杂度 。其中 为所有单词的长度之和。
class Solution { /* 首先,将单词数组中的每个单词按顺序加入到当前行中,直到超过了 maxWidth 的长度。 然后,根据单词数组中剩余的单词数量和需要填充的空格数量来计算每个单词之间需要填充的平均空格数和额外需要填充的空格数。 最后,将当前行按照左对齐或均匀分配空格的方式进行格式化。 */ public List<String> fullJustify(String[] words, int maxWidth) { List<String> result = new ArrayList<>(); int left = 0; // 当前行的第一个单词在 words 数组中的下标 while (left < words.length) { int right = left; // 当前行最后一个单词在 words 数组中的下标 int lineLength = words[right].length(); // 当前行所有单词(含空格)的总长度 while (right + 1 < words.length && lineLength + words[right + 1].length() + 1 <= maxWidth) { // 直到把下一个单词添加到当前行时行长度超过 maxWidth right++; lineLength += words[right].length() + 1; // 加上新单词的长度和一个空格的长度 } // 计算空格的数量和单词之间的平均空格数 int spaces = maxWidth - lineLength; int spaceCount = right - left; // 单词之间的空格数量 String line = words[left]; if (right == words.length - 1) { // 最后一行,单词之间不需要填充额外的空格 for (int i = left + 1; i <= right; i++) { line += " " + words[i]; } for (int i = line.length(); i < maxWidth; i++) { line += " "; } } else if (spaceCount == 0) { // 当前行只有一个单词 for (int i = line.length(); i < maxWidth; i++) { line += " "; } } else { int averageSpaceCount = spaces / spaceCount; // 单词之间平均需要填充的空格数 int extraSpaceCount = spaces % spaceCount; // 需要额外填充的空格数 for (int i = left + 1; i <= right; i++) { int spaceLength = averageSpaceCount + (extraSpaceCount-- > 0 ? 1 : 0); for (int j = 0; j < spaceLength; j++) { line += " "; } line += words[i]; } } result.add(line); left = right + 1; // 处理下一行 } return result; } }
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4 输出:2
示例 2:
输入:x = 8 输出:2 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1二分查找。
class Solution { public int mySqrt(int x) { int left = 0, right = x; while (left < right) { int mid = (left + right + 1) >>> 1; if (mid <= x / mid) { // mid*mid <= x left = mid; } else { right = mid - 1; } } return left; } }
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
提示:
1 <= n <= 45方法一:递推
我们定义 表示爬到第 阶楼梯的方法数,那么 可以由 和 转移而来,即:
初始条件为 ,,即爬到第 0 阶楼梯的方法数为 1,爬到第 1 阶楼梯的方法数也为 1。
答案即为 。
由于 只与 和 有关,因此我们可以只用两个变量 和 来维护当前的方法数,空间复杂度降低为 。
时间复杂度 ,空间复杂度 。
class Solution { public int climbStairs(int n) { int a = 0, b = 1; for (int i = 0; i < n; ++i) { int c = a + b; a = b; b = c; } return b; } }
给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
'/' 开头。'/' 。'/' 结尾。'.' 或 '..')。返回简化后得到的 规范路径 。
示例 1:
输入:path = "/home/" 输出:"/home" 解释:注意,最后一个目录名后面没有斜杠。
示例 2:
输入:path = "/../" 输出:"/" 解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:
输入:path = "/home//foo/" 输出:"/home/foo" 解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:
输入:path = "/a/./b/../../c/" 输出:"/c"
提示:
1 <= path.length <= 3000path 由英文字母,数字,'.','/' 或 '_' 组成。path 是一个有效的 Unix 风格绝对路径。方法一:栈
我们先将路径按照 '/' 分割成若干个子串,然后遍历每个子串,根据子串的内容进行如下操作:
'.',则不做任何操作,因为 '.' 表示当前目录;'..',则需要将栈顶元素弹出,因为 '..' 表示上一级目录;最后,我们将栈中的所有元素按照从栈底到栈顶的顺序拼接成字符串,即为简化后的规范路径。
时间复杂度 ,空间复杂度 。其中 为路径的长度。
class Solution { public String simplifyPath(String path) { Deque<String> stk = new ArrayDeque<>(); for (String s : path.split("/")) { if ("".equals(s) || ".".equals(s)) { continue; } if ("..".equals(s)) { stk.pollLast(); } else { stk.offerLast(s); } } return "/" + String.join("/", stk); } }
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
示例 1:
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500word1 和 word2 由小写英文字母组成动态规划。
设 dp[i][j] 表示将 word1 前 i 个字符组成的字符串 word1[0...i-1] 转换成 word2 前 j 个字符组成的字符串 word2[0...j-1] 的最小操作次数。m, n 分别表示 word1, word2 的长度。
初始化 dp[i][0] = i(i∈[0, m]),dp[0][j] = j (j∈[0, m])。
i, j 分别从 1 开始遍历,判断 word1[i - 1] 与 word2[j - 1] 是否相等:
word1[i - 1] == word2[j - 1],则 dp[i][j] = dp[i - 1][j - 1]。word1[i - 1] != word2[j - 1],则 dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1。其中 dp[i - 1][j] + 1 对应插入操作,dp[i][j - 1] + 1 对应删除操作,dp[i - 1][j - 1] + 1 对应替换操作。取三者的最小值即可。递推公式如下:

最后返回 dp[m][n] 即可。
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; ++i) { dp[i][0] = i; } for (int j = 0; j <= n; ++j) { dp[0][j] = j; } for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; } } } return dp[m][n]; } }
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] 输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
提示:
m == matrix.lengthn == matrix[0].length1 <= m, n <= 200-231 <= matrix[i][j] <= 231 - 1进阶:
O(mn) 的额外空间,但这并不是一个好的解决方案。O(m + n) 的额外空间,但这仍然不是最好的解决方案。方法一:数组标记
我们分别用数组 rows 和 cols 标记待清零的行和列。
然后再遍历一遍矩阵,将 rows 和 cols 中标记的行和列对应的元素清零。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵的行数和列数。
方法二:原地标记
方法一中使用了额外的数组标记待清零的行和列,实际上我们也可以直接用矩阵的第一行和第一列来标记,不需要开辟额外的数组空间。
由于第一行、第一列用来做标记,它们的值可能会因为标记而发生改变,因此,我们需要额外的变量 , 来标记第一行、第一列是否需要被清零。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵的行数和列数。
class Solution { public void setZeroes(int[][] matrix) { int m = matrix.length, n = matrix[0].length; boolean[] rows = new boolean[m]; boolean[] cols = new boolean[n]; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (matrix[i][j] == 0) { rows[i] = true; cols[j] = true; } } } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (rows[i] || cols[j]) { matrix[i][j] = 0; } } } } }
class Solution { public void setZeroes(int[][] matrix) { int m = matrix.length, n = matrix[0].length; boolean i0 = false, j0 = false; for (int j = 0; j < n; ++j) { if (matrix[0][j] == 0) { i0 = true; break; } } for (int i = 0; i < m; ++i) { if (matrix[i][0] == 0) { j0 = true; break; } } for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; } } } for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { if (matrix[i][0] == 0 || matrix[0][j] == 0) { matrix[i][j] = 0; } } } if (i0) { for (int j = 0; j < n; ++j) { matrix[0][j] = 0; } } if (j0) { for (int i = 0; i < m; ++i) { matrix[i][0] = 0; } } } }
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 输出:false
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 100-104 <= matrix[i][j], target <= 104方法一:二分查找
将二维矩阵逻辑展开,然后二分查找即可。
时间复杂度 。
方法二:从左下角或右上角搜索
这里我们以左下角作为起始搜索点,往右上方向开始搜索,比较当前元素 matrix[i][j] 与 target 的大小关系:
matrix[i][j] == target,说明找到了目标值,直接返回 true。matrix[i][j] > target,说明这一行从当前位置开始往右的所有元素均大于 target,应该让 i 指针往上移动,即 i--。matrix[i][j] < target,说明这一列从当前位置开始往上的所有元素均小于 target,应该让 j 指针往右移动,即 j++。若搜索结束依然找不到 target,返回 false。
时间复杂度 。
二分查找:
从左下角或右上角搜索:
二分查找:
class Solution { public boolean searchMatrix(int[][] matrix, int target) { int m = matrix.length, n = matrix[0].length; int left = 0, right = m * n - 1; while (left < right) { int mid = (left + right) >> 1; int x = mid / n, y = mid % n; if (matrix[x][y] >= target) { right = mid; } else { left = mid + 1; } } return matrix[left / n][left % n] == target; } }
从左下角或右上角搜索:
class Solution { public boolean searchMatrix(int[][] matrix, int target) { int m = matrix.length, n = matrix[0].length; for (int i = m - 1, j = 0; i >= 0 && j < n;) { if (matrix[i][j] == target) { return true; } if (matrix[i][j] > target) { --i; } else { ++j; } } return false; } }
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1] 输出:[0,1,2]
提示:
n == nums.length1 <= n <= 300nums[i] 为 0、1 或 2进阶:
计数:
nums,记录其中 0、1 和 2 出现的次数。nums。双指针:
数组元素只存在 0、1 和 2 三种,因此将 0 移动至数组头部,2 移动至数组尾部,排序便完成了。
0:与头指针数值交换,并向前一步,遍历指针向前。2:与尾指针数值交换,并向后一步。遍历指针不变(还需要处理交换上来的数值)。1:遍历指针向前。class Solution { public void sortColors(int[] nums) { int i = -1, j = nums.length; int cur = 0; while (cur < j) { if (nums[cur] == 0) { swap(nums, cur++, ++i); } else if (nums[cur] == 1) { ++cur; } else { swap(nums, cur, --j); } } } private void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } }
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。s 中存在这样的子串,我们保证它是唯一的答案。示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s 和 t 由英文字母组成进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
方法一:计数 + 双指针
我们用一个哈希表或数组 统计字符串 中每个字符出现的次数,用另一个哈希表或数组 统计滑动窗口中每个字符出现的次数。另外,定义两个指针 和 分别指向窗口的左右边界,变量 表示窗口中已经包含了 中的多少个字符,变量 和 分别表示最小覆盖子串的起始位置和长度。
我们从左到右遍历字符串 ,对于当前遍历到的字符 :
我们将其加入窗口中,即 ,如果此时 ,则说明 是一个「必要的字符」,我们将 加一。如果 等于 的长度,说明此时窗口中已经包含了 中的所有字符,我们就可以尝试更新最小覆盖子串的起始位置和长度了。如果 ,说明当前窗口表示的子串更短,我们就更新 和 。然后,我们尝试移动左边界 ,如果此时 ,则说明 是一个「必要的字符」,移动左边界时会把 这个字符从窗口中移除,因此我们需要将 减一,然后更新 ,并将 右移一位。如果 与 的长度不相等,说明此时窗口中还没有包含 中的所有字符,我们就不需要移动左边界了,直接将 右移一位,继续遍历即可。
遍历结束,如果没有找到最小覆盖子串,返回空字符串,否则返回 即可。
时间复杂度 ,空间复杂度 。其中 和 分别是字符串 和 的长度;而 是字符集的大小,本题中 。
class Solution { public String minWindow(String s, String t) { int[] need = new int[128]; int[] window = new int[128]; int m = s.length(), n = t.length(); for (int i = 0; i < n; ++i) { ++need[t.charAt(i)]; } int cnt = 0, j = 0, k = -1, mi = 1 << 30; for (int i = 0; i < m; ++i) { ++window[s.charAt(i)]; if (need[s.charAt(i)] >= window[s.charAt(i)]) { ++cnt; } while (cnt == n) { if (i - j + 1 < mi) { mi = i - j + 1; k = j; } if (need[s.charAt(j)] >= window[s.charAt(j)]) { --cnt; } --window[s.charAt(j++)]; } } return k < 0 ? "" : s.substring(k, k + mi); } }
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 201 <= k <= n方法一:回溯(两种方式)
我们设计一个函数 ,表示从数字 开始搜索,当前搜索路径为 ,答案为 。
函数 的执行逻辑如下:
以上做法实际上是枚举当前数字选或者不选,然后递归地搜索下一个数字。我们也可以枚举下一个要选择的数字 ,其中 ,如果下一个要选择的数字是 ,那么我们将数字 加入搜索路径 ,然后继续搜索,即执行 ,接着将数字 从搜索路径 中移除。
在主函数中,我们从数字 开始搜索,即执行 。
时间复杂度 ,空间复杂度 。其中 表示组合数。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int n; private int k; public List<List<Integer>> combine(int n, int k) { this.n = n; this.k = k; dfs(1); return ans; } private void dfs(int i) { if (t.size() == k) { ans.add(new ArrayList<>(t)); return; } if (i > n) { return; } t.add(i); dfs(i + 1); t.remove(t.size() - 1); dfs(i + 1); } }
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int n; private int k; public List<List<Integer>> combine(int n, int k) { this.n = n; this.k = k; dfs(1); return ans; } private void dfs(int i) { if (t.size() == k) { ans.add(new ArrayList<>(t)); return; } if (i > n) { return; } for (int j = i; j <= n; ++j) { t.add(j); dfs(j + 1); t.remove(t.size() - 1); } } }
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10nums 中的所有元素 互不相同方法一:递归枚举
我们设计一个递归函数 ,它的参数为当前枚举到的元素的下标 ,以及当前的子集 。
当前枚举到的元素下标为 ,我们可以选择将其加入子集 中,也可以选择不加入子集 中。递归这两种选择,即可得到所有的子集。
时间复杂度 ,空间复杂度 。其中 为数组的长度。数组中每个元素有两种状态,即选择或不选择,共 种状态,每种状态需要 的时间来构造子集。
方法二:二进制枚举
我们可以将方法一中的递归过程改写成迭代的形式,即使用二进制枚举的方法来枚举所有的子集。
我们可以使用 个二进制数来表示 个元素的所有子集,若某个二进制数 mask 的第 位为 ,表示子集中包含数组第 个元素 ;若为 ,表示子集中不包含数组第 个元素 。
时间复杂度 ,空间复杂度 。其中 为数组的长度。一共有 个子集,每个子集需要 的时间来构造。
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private int[] nums; public List<List<Integer>> subsets(int[] nums) { this.nums = nums; dfs(0, new ArrayList<>()); return ans; } private void dfs(int u, List<Integer> t) { if (u == nums.length) { ans.add(new ArrayList<>(t)); return; } dfs(u + 1, t); t.add(nums[u]); dfs(u + 1, t); t.remove(t.size() - 1); } }
class Solution { public List<List<Integer>> subsets(int[] nums) { int n = nums.length; List<List<Integer>> ans = new ArrayList<>(); for (int mask = 0; mask < 1 << n; ++mask) { List<Integer> t = new ArrayList<>(); for (int i = 0; i < n; ++i) { if (((mask >> i) & 1) == 1) { t.add(nums[i]); } } ans.add(t); } return ans; } }
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" 输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" 输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB" 输出:false
提示:
m == board.lengthn = board[i].length1 <= m, n <= 61 <= word.length <= 15board 和 word 仅由大小写英文字母组成进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
回溯(深度优先搜索 DFS )实现。
class Solution { public boolean exist(char[][] board, String word) { int m = board.length; int n = board[0].length; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (dfs(i, j, 0, m, n, board, word)) { return true; } } } return false; } private boolean dfs(int i, int j, int cur, int m, int n, char[][] board, String word) { if (cur == word.length()) { return true; } if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word.charAt(cur)) { return false; } board[i][j] += 256; int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (dfs(x, y, cur + 1, m, n, board, word)) { return true; } } board[i][j] -= 256; return false; } }
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [1,1,1,2,2,3] 输出:5, nums = [1,1,2,2,3] 解释:函数应返回新长度 length =5, 并且原数组的前五个元素被修改为1, 1, 2, 2,3 。 不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,1,2,3,3] 输出:7, nums = [0,0,1,1,2,3,3] 解释:函数应返回新长度 length =7, 并且原数组的前五个元素被修改为0, 0, 1, 1, 2, 3, 3 。 不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 104-104 <= nums[i] <= 104nums 已按升序排列方法一:一次遍历
我们用一个变量 记录当前已经处理好的数组的长度,初始时 ,表示空数组。
然后我们从左到右遍历数组,对于遍历到的每个元素 ,如果 或者 ,我们就将 放到 的位置,然后 自增 。否则, 与 相同,我们直接跳过这个元素。继续遍历,直到遍历完整个数组。
这样,当遍历结束时, 中前 个元素就是我们要求的答案,且 就是答案的长度。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
补充:
原问题要求最多相同的数字最多出现 次,我们可以扩展至相同的数字最多保留 个。
相似题目:26. 删除有序数组中的重复项
class Solution { public int removeDuplicates(int[] nums) { int k = 0; for (int x : nums) { if (k < 2 || x != nums[k - 2]) { nums[k++] = x; } } return k; } }
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
你必须尽可能减少整个操作步骤。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0 输出:true
示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3 输出:false
提示:
1 <= nums.length <= 5000-104 <= nums[i] <= 104nums 在预先未知的某个下标上进行了旋转-104 <= target <= 104进阶:
nums 可能包含重复元素。方法一:二分查找
class Solution { public boolean search(int[] nums, int target) { int l = 0, r = nums.length - 1; while (l <= r) { int mid = (l + r) >>> 1; if (nums[mid] == target) return true; if (nums[mid] < nums[r] || nums[mid] < nums[l]) { if (target > nums[mid] && target <= nums[r]) l = mid + 1; else r = mid - 1; } else if (nums[mid] > nums[l] || nums[mid] > nums[r]) { if (target < nums[mid] && target >= nums[l]) r = mid - 1; else l = mid + 1; } else r--; } return false; } }
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:
输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3] 输出:[2,3]
提示:
[0, 300] 内-100 <= Node.val <= 100方法一:一次遍历
我们先创建一个虚拟头节点 ,令 ,然后创建指针 指向 ,指针 指向 ,开始遍历链表。
当 指向的节点值与 指向的节点值相同时,我们就让 不断向后移动,直到 指向的节点值与 指向的节点值不相同时,停止移动。此时,我们判断 是否等于 ,如果相等,说明 与 之间没有重复节点,我们就让 移动到 的位置;否则,说明 与 之间有重复节点,我们就让 指向 。然后让 继续向后移动。继续上述操作,直到 为空,遍历结束。
最后,返回 即可。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode pre = dummy; ListNode cur = head; while (cur != null) { while (cur.next != null && cur.next.val == cur.val) { cur = cur.next; } if (pre.next == cur) { pre = cur; } else { pre.next = cur.next; } cur = cur.next; } return dummy.next; } }
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2] 输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3] 输出:[1,2,3]
提示:
[0, 300] 内-100 <= Node.val <= 100class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode cur = head; while (cur != null && cur.next != null) { if (cur.val == cur.next.val) { cur.next = cur.next.next; } else { cur = cur.next; } } return head; } }
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:

输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10
示例 2:

输入: heights = [2,4] 输出: 4
提示:
1 <= heights.length <=1050 <= heights[i] <= 104方法一:单调栈
单调栈常见模型:找出每个数左/右边离它最近的且比它大/小的数。模板:
枚举每根柱子的高度 作为矩形的高度,向左右两边找第一个高度小于 的下标 , 。那么此时矩形面积为 ,求最大值即可。
时间复杂度 ,其中 表示 的长度。
class Solution { public int largestRectangleArea(int[] heights) { int res = 0, n = heights.length; Deque<Integer> stk = new ArrayDeque<>(); int[] left = new int[n]; int[] right = new int[n]; Arrays.fill(right, n); for (int i = 0; i < n; ++i) { while (!stk.isEmpty() && heights[stk.peek()] >= heights[i]) { right[stk.pop()] = i; } left[i] = stk.isEmpty() ? -1 : stk.peek(); stk.push(i); } for (int i = 0; i < n; ++i) { res = Math.max(res, heights[i] * (right[i] - left[i] - 1)); } return res; } }
给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:6 解释:最大矩形如上图所示。
示例 2:
输入:matrix = [] 输出:0
示例 3:
输入:matrix = [["0"]] 输出:0
示例 4:
输入:matrix = [["1"]] 输出:1
示例 5:
输入:matrix = [["0","0"]] 输出:0
提示:
rows == matrix.lengthcols == matrix[0].length1 <= row, cols <= 200matrix[i][j] 为 '0' 或 '1'方法一:单调栈
把每一行视为柱状图的底部,对每一行求柱状图的最大面积即可。
时间复杂度 ,其中 表示 的行数, 表示 的列数。
class Solution { public int maximalRectangle(char[][] matrix) { int n = matrix[0].length; int[] heights = new int[n]; int ans = 0; for (var row : matrix) { for (int j = 0; j < n; ++j) { if (row[j] == '1') { heights[j] += 1; } else { heights[j] = 0; } } ans = Math.max(ans, largestRectangleArea(heights)); } return ans; } private int largestRectangleArea(int[] heights) { int res = 0, n = heights.length; Deque<Integer> stk = new ArrayDeque<>(); int[] left = new int[n]; int[] right = new int[n]; Arrays.fill(right, n); for (int i = 0; i < n; ++i) { while (!stk.isEmpty() && heights[stk.peek()] >= heights[i]) { right[stk.pop()] = i; } left[i] = stk.isEmpty() ? -1 : stk.peek(); stk.push(i); } for (int i = 0; i < n; ++i) { res = Math.max(res, heights[i] * (right[i] - left[i] - 1)); } return res; } }
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5]
示例 2:
输入:head = [2,1], x = 2 输出:[1,2]
提示:
[0, 200] 内-100 <= Node.val <= 100-200 <= x <= 200方法一:模拟
创建两个链表,一个存放小于 x 的节点,另一个存放大于等于 x 的节点,之后进行拼接即可。
时间复杂度 O(1)n$ 是原链表的长度。
class Solution { public ListNode partition(ListNode head, int x) { ListNode d1 = new ListNode(); ListNode d2 = new ListNode(); ListNode t1 = d1, t2 = d2; while (head != null) { if (head.val < x) { t1.next = head; t1 = t1.next; } else { t2.next = head; t2 = t2.next; } head = head.next; } t1.next = d2.next; t2.next = null; return d1.next; } }
使用下面描述的算法可以扰乱字符串 s 得到字符串 t :
s ,则可以将其分成两个子字符串 x 和 y ,且满足 s = x + y 。s 可能是 s = x + y 或者 s = y + x 。x 和 y 这两个子字符串上继续从步骤 1 开始递归执行此算法。给你两个 长度相等 的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:s1 = "great", s2 = "rgeat" 输出:true 解释:s1 上可能发生的一种情形是: "great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串 "gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」 "gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割 "g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」 "r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t" "r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」 算法终止,结果字符串和 s2 相同,都是 "rgeat" 这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true
示例 2:
输入:s1 = "abcde", s2 = "caebd" 输出:false
示例 3:
输入:s1 = "a", s2 = "a" 输出:true
提示:
s1.length == s2.length1 <= s1.length <= 30s1 和 s2 由小写英文字母组成方法一:记忆化搜索
我们设计一个函数 ,表示字符串 从 开始长度为 的子串是否能变换为字符串 从 开始长度为 的子串。如果能变换,返回 true,否则返回 false。那么答案就是 ,其中 是字符串的长度。
函数 的计算方式如下:
true,否则返回 false;true,否则返回 false。最后,我们返回 。
为了避免重复计算,我们可以使用记忆化搜索的方式。
时间复杂度 ,空间复杂度 。其中 是字符串的长度。
方法二:动态规划(区间 DP)
我们定义 表示字符串 从 开始长度为 的子串是否能变换为字符串 从 开始长度为 的子串。那么答案就是 ,其中 是字符串的长度。
对于长度为 的子串,如果 ,那么 ,否则 。
接下来,我们从小到大枚举子串的长度 ,从 开始枚举 ,从 开始枚举 ,如果 或者 成立,那么 也成立。
最后,我们返回 。
时间复杂度 ,空间复杂度 。其中 是字符串的长度。
class Solution { private Boolean[][][] f; private String s1; private String s2; public boolean isScramble(String s1, String s2) { int n = s1.length(); this.s1 = s1; this.s2 = s2; f = new Boolean[n][n][n + 1]; return dfs(0, 0, n); } private boolean dfs(int i, int j, int k) { if (f[i][j][k] != null) { return f[i][j][k]; } if (k == 1) { return s1.charAt(i) == s2.charAt(j); } for (int h = 1; h < k; ++h) { if (dfs(i, j, h) && dfs(i + h, j + h, k - h)) { return f[i][j][k] = true; } if (dfs(i + h, j, k - h) && dfs(i, j + k - h, h)) { return f[i][j][k] = true; } } return f[i][j][k] = false; } }
class Solution { public boolean isScramble(String s1, String s2) { int n = s1.length(); boolean[][][] f = new boolean[n][n][n + 1]; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { f[i][j][1] = s1.charAt(i) == s2.charAt(j); } } for (int k = 2; k <= n; ++k) { for (int i = 0; i <= n - k; ++i) { for (int j = 0; j <= n - k; ++j) { for (int h = 1; h < k; ++h) { if (f[i][j][h] && f[i + h][j + h][k - h]) { f[i][j][k] = true; break; } if (f[i + h][j][k - h] && f[i][j + k - h][h]) { f[i][j][k] = true; break; } } } } } return f[0][0][n]; } }
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1] 解释:需要合并 [1] 和 [] 。 合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1 输出:[1] 解释:需要合并的数组是 [] 和 [1] 。 合并结果是 [1] 。 注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示:
nums1.length == m + nnums2.length == n0 <= m, n <= 2001 <= m + n <= 200-109 <= nums1[i], nums2[j] <= 109进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
方法一:双指针
我们用两个指针 和 分别指向两个数组的末尾,用一个指针 指向合并后的数组的末尾。
每次比较两个数组的末尾元素,将较大的元素放在合并后的数组的末尾,然后将指针向前移动一位,重复这个过程,直到两个数组的指针都指向了数组的开头。
时间复杂度 ,其中 和 分别是两个数组的长度。空间复杂度 。
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { for (int i = m - 1, j = n - 1, k = m + n - 1; j >= 0; --k) { nums1[k] = i >= 0 && nums1[i] > nums2[j] ? nums1[i--] : nums2[j--]; } } }
n 位格雷码序列 是一个由 2n 个整数组成的序列,其中:
[0, 2n - 1] 内(含 0 和 2n - 1)0给你一个整数 n ,返回任一有效的 n 位格雷码序列 。
示例 1:
输入:n = 2 输出:[0,1,3,2] 解释: [0,1,3,2] 的二进制表示是 [00,01,11,10] 。 - 00 和 01 有一位不同 - 01 和 11 有一位不同 - 11 和 10 有一位不同 - 10 和 00 有一位不同 [0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。 - 00 和 10 有一位不同 - 10 和 11 有一位不同 - 11 和 01 有一位不同 - 01 和 00 有一位不同
示例 2:
输入:n = 1 输出:[0,1]
提示:
1 <= n <= 16方法一:二进制码转格雷码
格雷码是我们在工程中常会遇到的一种编码方式,它的基本的特点就是任意两个相邻的代码只有一位二进制数不同。
二进制码转换成二进制格雷码,其法则是保留二进制码的最高位作为格雷码的最高位,而次高位格雷码为二进制码的高位与次高位相异或,而格雷码其余各位与次高位的求法相类似。
假设某个二进制数表示为 ,其格雷码表示为 。最高位保留,所以 ;而其它各位 ,其中 。
因此,对于一个整数 ,我们可以用函数 得到其格雷码:
int gray(x) { return x ^ (x >> 1); }
我们直接将 这些整数映射成对应的格雷码,即可得到答案数组。
时间复杂度 ,其中 为题目给定的整数。忽略答案的空间消耗,空间复杂度 。
class Solution { public List<Integer> grayCode(int n) { List<Integer> ans = new ArrayList<>(); for (int i = 0; i < 1 << n; ++i) { ans.add(i ^ (i >> 1)); } return ans; } }
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10class Solution { private List<List<Integer>> ans; private int[] nums; public List<List<Integer>> subsetsWithDup(int[] nums) { ans = new ArrayList<>(); Arrays.sort(nums); this.nums = nums; dfs(0, new ArrayList<>()); return ans; } private void dfs(int u, List<Integer> t) { ans.add(new ArrayList<>(t)); for (int i = u; i < nums.length; ++i) { if (i != u && nums[i] == nums[i - 1]) { continue; } t.add(nums[i]); dfs(i + 1, t); t.remove(t.size() - 1); } } }
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
'A' -> "1" 'B' -> "2" ... 'Z' -> "26"
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:
"AAJF" ,将消息分组为 (1 1 10 6)"KJF" ,将消息分组为 (11 10 6)注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12" 输出:2 解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226" 输出:3 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "06" 输出:0 解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
提示:
1 <= s.length <= 100s 只包含数字,并且可能包含前导零。动态规划法。
假设 dp[i] 表示字符串 s 的前 i 个字符 s[1..i] 的解码方法数。
考虑最后一次解码中使用了 s 中的哪些字符:
s[i] 进行解码,那么只要 s[i]≠0,它就可以被解码成 A∼I 中的某个字母。由于剩余的前 i-1 个字符的解码方法数为 dp[i-1],所以 dp[i] = dp[i-1]。s[i-1] 和 s[i] 进行编码。与第一种情况类似,s[i-1] 不能等于 0,并且 s[i-1] 和 s[i] 组成的整数必须小于等于 26,这样它们就可以被解码成 J∼Z 中的某个字母。由于剩余的前 i-2 个字符的解码方法数为 dp[i-2],所以 dp[i] = dp[i-2]。将上面的两种状态转移方程在对应的条件满足时进行累加,即可得到 dp[i]的值。在动态规划完成后,最终的答案即为 dp[n]。
由于 dp[i] 的值仅与 dp[i-1] 和 dp[i-2] 有关,因此可以不定义 dp 数组,可以仅使用三个变量进行状态转移。
优化空间:
class Solution { public int numDecodings(String s) { int n = s.length(); int[] dp = new int[n + 1]; dp[0] = 1; for (int i = 1; i <= n; ++i) { if (s.charAt(i - 1) != '0') { dp[i] += dp[i - 1]; } if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + s.charAt(i - 1) - '0') <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; } }
优化空间:
class Solution { public int numDecodings(String s) { int n = s.length(); int a = 0, b = 1, c = 0; for (int i = 1; i <= n; ++i) { c = 0; if (s.charAt(i - 1) != '0') { c += b; } if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + s.charAt(i - 1) - '0') <= 26) { c += a; } a = b; b = c; } return c; } }
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5]
示例 2:
输入:head = [5], left = 1, right = 1 输出:[5]
提示:
n1 <= n <= 500-500 <= Node.val <= 5001 <= left <= right <= n进阶: 你可以使用一趟扫描完成反转吗?
方法一:模拟
定义一个虚拟头结点 dummy,指向链表的头结点 head,然后定义一个指针 pre 指向 dummy,从虚拟头结点开始遍历链表,遍历到第 left 个结点时,将 pre 指向该结点,然后从该结点开始遍历 right - left + 1 次,将遍历到的结点依次插入到 pre 的后面,最后返回 dummy.next 即可。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
class Solution { public ListNode reverseBetween(ListNode head, int left, int right) { if (head.next == null || left == right) { return head; } ListNode dummy = new ListNode(0, head); ListNode pre = dummy; for (int i = 0; i < left - 1; ++i) { pre = pre.next; } ListNode p = pre; ListNode q = pre.next; ListNode cur = q; for (int i = 0; i < right - left + 1; ++i) { ListNode t = cur.next; cur.next = pre; pre = cur; cur = t; } p.next = pre; q.next = cur; return dummy.next; } }
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000" 输出:["0.0.0.0"]
示例 3:
输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示:
1 <= s.length <= 20s 仅由数字组成DFS。
class Solution { private List<String> ans; public List<String> restoreIpAddresses(String s) { ans = new ArrayList<>(); dfs(s, new ArrayList<>()); return ans; } private void dfs(String s, List<String> t) { if (t.size() == 4) { if ("".equals(s)) { ans.add(String.join(".", t)); } return; } for (int i = 1; i < Math.min(4, s.length() + 1); ++i) { String c = s.substring(0, i); if (check(c)) { t.add(c); dfs(s.substring(i), t); t.remove(t.size() - 1); } } } private boolean check(String s) { if ("".equals(s)) { return false; } int num = Integer.parseInt(s); if (num > 255) { return false; } if (s.charAt(0) == '0' && s.length() > 1) { return false; } return true; } }
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3] 输出:[1,3,2]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [1] 输出:[1]
提示:
[0, 100] 内-100 <= Node.val <= 100进阶: 递归算法很简单,你可以通过迭代算法完成吗?
1. 递归遍历
先递归左子树,再访问根节点,接着递归右子树。
2. 栈实现非递归遍历
非递归的思路如下:
3. Morris 实现中序遍历
Morris 遍历无需使用栈,空间复杂度为 O(1)。核心思想是:
遍历二叉树节点,
root.rightroot.left。root.right。递归:
栈实现非递归:
Morris 遍历:
递归:
class Solution { private List<Integer> ans; public List<Integer> inorderTraversal(TreeNode root) { ans = new ArrayList<>(); dfs(root); return ans; } private void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); ans.add(root.val); dfs(root.right); } }
栈实现非递归:
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); Deque<TreeNode> stk = new ArrayDeque<>(); while (root != null || !stk.isEmpty()) { if (root != null) { stk.push(root); root = root.left; } else { root = stk.pop(); ans.add(root.val); root = root.right; } } return ans; } }
Morris 遍历:
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); while (root != null) { if (root.left == null) { ans.add(root.val); root = root.right; } else { TreeNode prev = root.left; while (prev.right != null && prev.right != root) { prev = prev.right; } if (prev.right == null) { prev.right = root; root = root.left; } else { ans.add(root.val); prev.right = null; root = root.right; } } } return ans; } } ```# [95. 不同的二叉搜索树 II](https://leetcode.cn/problems/unique-binary-search-trees-ii) ## 题目描述 <p class="mume-header " id="题目描述-94"></p> <p>给你一个整数 <code>n</code> ,请你生成并返回所有由 <code>n</code> 个节点组成且节点值从 <code>1</code> 到 <code>n</code> 互不相同的不同 <strong>二叉搜索树</strong><em> </em>。可以按 <strong>任意顺序</strong> 返回答案。</p> <div class="original__bRMd"> <div> <p><strong>示例 1:</strong></p> <img alt="" src="https://gcore.jsdelivr.net/gh/doocs/leetcode@main/solution/0000-0099/0095.Unique%20Binary%20Search%20Trees%20II/images/uniquebstn3.jpg" style="width: 600px; height: 148px;" /> <pre> <strong>输入:</strong>n = 3 <strong>输出:</strong>[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]] </pre> <p><strong>示例 2:</strong></p> <pre> <strong>输入:</strong>n = 1 <strong>输出:</strong>[[1]] </pre> <p><strong>提示:</strong></p> <ul> <li><code>1 <= n <= 8</code></li> </ul> </div> </div> ## 解法 <p class="mume-header " id="解法-94"></p> ### **Java** <p class="mume-header " id="java-94"></p> ```java class Solution { public List<TreeNode> generateTrees(int n) { return generateTrees(1, n); } private List<TreeNode> generateTrees(int left, int right) { List<TreeNode> ans = new ArrayList<>(); if (left > right) { ans.add(null); } else { for (int i = left; i <= right; ++i) { List<TreeNode> leftTrees = generateTrees(left, i - 1); List<TreeNode> rightTrees = generateTrees(i + 1, right); for (TreeNode l : leftTrees) { for (TreeNode r : rightTrees) { TreeNode node = new TreeNode(i, l, r); ans.add(node); } } } } return ans; } }
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3 输出:5
示例 2:
输入:n = 1 输出:1
提示:
1 <= n <= 19假设 n 个节点存在二叉搜索树的个数是 G(n),1 为根节点,2 为根节点,...,n 为根节点,当 1 为根节点时,其左子树节点个数为 0,右子树节点个数为 n-1,同理当 2 为根节点时,其左子树节点个数为 1,右子树节点为 n-2,所以可得 G(n) = G(0) * G(n-1) + G(1) * (n-2) + ... + G(n-1) * G(0)。
class Solution { public int numTrees(int n) { int[] dp = new int[n + 1]; dp[0] = 1; for (int i = 1; i <= n; ++i) { for (int j = 0; j < i; ++j) { dp[i] += dp[j] * dp[i - j - 1]; } } return dp[n]; } }
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + snt = t1 + t2 + ... + tm|n - m| <= 1s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...注意:a + b 意味着字符串 a 和 b 连接。
示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" 输出:true
示例 2:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc" 输出:false
示例 3:
输入:s1 = "", s2 = "", s3 = "" 输出:true
提示:
0 <= s1.length, s2.length <= 1000 <= s3.length <= 200s1、s2、和 s3 都由小写英文字母组成进阶:您能否仅使用 O(s2.length) 额外的内存空间来解决它?
题目描述带有一定迷惑性,“交错”的过程其实就类似归并排序的 merge 过程,每次从 s1 或 s2 的首部取一个字符,最终组成 s3,用记忆化搜索或者动态规划都可以解决。
class Solution { private int m; private int n; private String s1; private String s2; private String s3; private Map<Integer, Boolean> memo = new HashMap<>(); public boolean isInterleave(String s1, String s2, String s3) { m = s1.length(); n = s2.length(); this.s1 = s1; this.s2 = s2; this.s3 = s3; if (m + n != s3.length()) { return false; } return dfs(0, 0); } private boolean dfs(int i, int j) { System.out.println(i + ", " + j); if (i == m && j == n) { return true; } if (memo.containsKey(i * 100 + j)) { return memo.get(i * 100 + j); } boolean ret = (i < m && s1.charAt(i) == s3.charAt(i + j) && dfs(i + 1, j)) || (j < n && s2.charAt(j) == s3.charAt(i + j) && dfs(i, j + 1)); memo.put(i * 100 + j, ret); return ret; } }
class Solution { public boolean isInterleave(String s1, String s2, String s3) { int m = s1.length(), n = s2.length(); if (m + n != s3.length()) { return false; } boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 0; i <= m; ++i) { for (int j = 0; j <= n; ++j) { int k = i + j - 1; if (i > 0) { dp[j] &= (s1.charAt(i - 1) == s3.charAt(k)); } if (j > 0) { dp[j] |= (s2.charAt(j - 1) == s3.charAt(k) && dp[j - 1]); } } } return dp[n]; } }
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
示例 1:
输入:root = [2,1,3] 输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
[1, 104] 内-231 <= Node.val <= 231 - 1方法一:递归
中序遍历,若是一个有效的二叉搜索树,那么遍历到的序列应该是单调递增的。所以只要比较判断遍历到的当前数是否大于上一个数即可。
或者考虑以 root 为根的子树,所有节点的值是否都在合法范围内,递归判断即可。
时间复杂度 ,空间复杂度 。其中 是树中节点的数量。
class Solution { private Integer prev; public boolean isValidBST(TreeNode root) { prev = null; return dfs(root); } private boolean dfs(TreeNode root) { if (root == null) { return true; } if (!dfs(root.left)) { return false; } if (prev != null && prev >= root.val) { return false; } prev = root.val; if (!dfs(root.right)) { return false; } return true; } }
class Solution { public boolean isValidBST(TreeNode root) { return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE); } private boolean dfs(TreeNode root, long l, long r) { if (root == null) { return true; } if (root.val <= l || root.val >= r) { return false; } return dfs(root.left, l, root.val) && dfs(root.right, root.val, r); } }
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
示例 1:
输入:root = [1,3,null,null,2] 输出:[3,1,null,null,2] 解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
示例 2:
输入:root = [3,1,4,null,null,2] 输出:[2,1,4,null,null,3] 解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
提示:
[2, 1000] 内-231 <= Node.val <= 231 - 1进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
方法一:中序遍历
中序遍历二叉搜索树,得到的序列是递增的。如果有两个节点的值被错误地交换,那么中序遍历得到的序列中,一定会出现两个逆序对。我们用 first 和 second 分别记录这两个逆序对中较小值和较大值的节点,最后交换这两个节点的值即可。
时间复杂度 ,空间复杂度 。其中 是二叉搜索树的节点个数。
class Solution { private TreeNode prev; private TreeNode first; private TreeNode second; public void recoverTree(TreeNode root) { dfs(root); int t = first.val; first.val = second.val; second.val = t; } private void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); if (prev != null && prev.val > root.val) { if (first == null) { first = prev; } second = root; } prev = root; dfs(root.right); } }
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入:p = [1,2,3], q = [1,2,3] 输出:true
示例 2:
输入:p = [1,2], q = [1,null,2] 输出:false
示例 3:
输入:p = [1,2,1], q = [1,1,2] 输出:false
提示:
[0, 100] 内-104 <= Node.val <= 104方法一:DFS
我们可以使用 DFS 递归的方法来解决这个问题。
首先判断两个二叉树的根节点是否相同,如果两个根节点都为空,则两个二叉树相同,如果两个根节点中有且只有一个为空,则两个二叉树一定不同。如果两个根节点都不为空,则判断它们的值是否相同,如果不相同则两个二叉树一定不同,如果相同,则分别判断两个二叉树的左子树是否相同以及右子树是否相同。当以上所有条件都满足时,两个二叉树才相同。
时间复杂度 ,空间复杂度 。其中 和 分别是两个二叉树的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的节点个数。
方法二:BFS
我们也可以使用 BFS 迭代的方法来解决这个问题。
首先将两个二叉树的根节点分别加入两个队列。每次从两个队列各取出一个节点,进行如下比较操作。如果两个节点的值不相同,则两个二叉树的结构一定不同,如果两个节点的值相同,则判断两个节点的子节点是否为空,如果只有一个节点的左子节点为空,则两个二叉树的结构一定不同,如果只有一个节点的右子节点为空,则两个二叉树的结构一定不同,如果左右子节点的结构相同,则将两个节点的左子节点和右子节点分别加入两个队列,对于下一次迭代,将从两个队列各取出一个节点进行比较。当两个队列同时为空时,说明我们已经比较完了所有节点,两个二叉树的结构完全相同。
时间复杂度 ,空间复杂度 。其中 和 分别是两个二叉树的节点个数。空间复杂度主要取决于队列中的元素个数,队列中的元素个数不会超过较小的二叉树的节点个数。
DFS:
BFS:
class Solution { public boolean isSameTree(TreeNode p, TreeNode q) { if (p == q) return true; if (p == null || q == null || p.val != q.val) return false; return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); } }
class Solution { public boolean isSameTree(TreeNode p, TreeNode q) { if (p == q) { return true; } if (p == null || q == null) { return false; } Deque<TreeNode> q1 = new ArrayDeque<>(); Deque<TreeNode> q2 = new ArrayDeque<>(); q1.offer(p); q2.offer(q); while (!q1.isEmpty() && !q2.isEmpty()) { p = q1.poll(); q = q2.poll(); if (p.val != q.val) { return false; } TreeNode la = p.left, ra = p.right; TreeNode lb = q.left, rb = q.right; if ((la != null && lb == null) || (lb != null && la == null)) { return false; } if ((ra != null && rb == null) || (rb != null && ra == null)) { return false; } if (la != null) { q1.offer(la); q2.offer(lb); } if (ra != null) { q1.offer(ra); q2.offer(rb); } } return true; } }
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3] 输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3] 输出:false
提示:
[1, 1000] 内-100 <= Node.val <= 100进阶:你可以运用递归和迭代两种方法解决这个问题吗?
方法一:递归
我们设计一个函数 ,用于判断两个二叉树是否对称。答案即为 。
函数 的逻辑如下:
true;false;时间复杂度 ,空间复杂度 。其中 是二叉树的节点数。
class Solution { public boolean isSymmetric(TreeNode root) { return dfs(root, root); } private boolean dfs(TreeNode root1, TreeNode root2) { if (root1 == null && root2 == null) { return true; } if (root1 == null || root2 == null || root1.val != root2.val) { return false; } return dfs(root1.left, root2.right) && dfs(root1.right, root2.left); } }
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1] 输出:[[1]]
示例 3:
输入:root = [] 输出:[]
提示:
[0, 2000] 内-1000 <= Node.val <= 1000方法一:BFS
我们可以使用 BFS 的方法来解决这道题。首先将根节点入队,然后不断地进行以下操作,直到队列为空:
最后返回答案数组即可。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root); while (!q.isEmpty()) { List<Integer> t = new ArrayList<>(); for (int n = q.size(); n > 0; --n) { TreeNode node = q.poll(); t.add(node.val); if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } ans.add(t); } return ans; } }
给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[3],[20,9],[15,7]]
示例 2:
输入:root = [1] 输出:[[1]]
示例 3:
输入:root = [] 输出:[]
提示:
[0, 2000] 内-100 <= Node.val <= 100方法一:BFS
为了实现锯齿形层序遍历,需要在层序遍历的基础上增加一个标志位 left,用于标记当前层的节点值的顺序。如果 left 为 true,则当前层的节点值按照从左到右的顺序存入结果数组 ans 中;如果 left 为 false,则当前层的节点值按照从右到左的顺序存入结果数组 ans 中。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点数。
class Solution { public List<List<Integer>> zigzagLevelOrder(TreeNode root) { List<List<Integer>> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root); boolean left = true; while (!q.isEmpty()) { List<Integer> t = new ArrayList<>(); for (int n = q.size(); n > 0; --n) { TreeNode node = q.poll(); t.add(node.val); if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } if (!left) { Collections.reverse(t); } ans.add(t); left = !left; } return ans; } }
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
方法一:递归
递归遍历左右子树,求左右子树的最大深度,然后取最大值加 即可。
时间复杂度 ,其中 是二叉树的节点数。每个节点在递归中只被遍历一次。
class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } int l = maxDepth(root.left); int r = maxDepth(root.right); return 1 + Math.max(l, r); } }
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1] 输出: [-1]
提示:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder 和 inorder 均 无重复 元素inorder 均出现在 preorderpreorder 保证 为二叉树的前序遍历序列inorder 保证 为二叉树的中序遍历序列方法一:递归
前序序列的第一个结点 为根节点,我们在中序序列中找到根节点的位置 ,可以将中序序列划分为左子树 、右子树 。
通过左右子树的区间,可以计算出左、右子树节点的个数,假设为 和 。然后在前序节点中,从根节点往后的 个节点为左子树,再往后的 个节点为右子树。
递归求解即可。
前序遍历:先遍历根节点,再遍历左右子树;中序遍历:先遍历左子树,再遍历根节点,最后遍历右子树。
时间复杂度 ,空间复杂度 。
class Solution { private Map<Integer, Integer> indexes = new HashMap<>(); public TreeNode buildTree(int[] preorder, int[] inorder) { for (int i = 0; i < inorder.length; ++i) { indexes.put(inorder[i], i); } return dfs(preorder, inorder, 0, 0, preorder.length); } private TreeNode dfs(int[] preorder, int[] inorder, int i, int j, int n) { if (n <= 0) { return null; } int v = preorder[i]; int k = indexes.get(v); TreeNode root = new TreeNode(v); root.left = dfs(preorder, inorder, i + 1, j, k - j); root.right = dfs(preorder, inorder, i + 1 + k - j, k + 1, n - k + j - 1); return root; } }
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
示例 1:
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] 输出:[3,9,20,null,null,15,7]
示例 2:
输入:inorder = [-1], postorder = [-1] 输出:[-1]
提示:
1 <= inorder.length <= 3000postorder.length == inorder.length-3000 <= inorder[i], postorder[i] <= 3000inorder 和 postorder 都由 不同 的值组成postorder 中每一个值都在 inorder 中inorder 保证是树的中序遍历postorder 保证是树的后序遍历方法一:递归
思路同 105. 从前序与中序遍历序列构造二叉树。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { private Map<Integer, Integer> indexes = new HashMap<>(); public TreeNode buildTree(int[] inorder, int[] postorder) { for (int i = 0; i < inorder.length; ++i) { indexes.put(inorder[i], i); } return dfs(inorder, postorder, 0, 0, inorder.length); } private TreeNode dfs(int[] inorder, int[] postorder, int i, int j, int n) { if (n <= 0) { return null; } int v = postorder[j + n - 1]; int k = indexes.get(v); TreeNode root = new TreeNode(v); root.left = dfs(inorder, postorder, i, j, k - i); root.right = dfs(inorder, postorder, k + 1, j + k - i, n - k + i - 1); return root; } }
给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[15,7],[9,20],[3]]
示例 2:
输入:root = [1] 输出:[[1]]
示例 3:
输入:root = [] 输出:[]
提示:
[0, 2000] 内-1000 <= Node.val <= 1000同 102,最后反转一下结果即可。
class Solution { public List<List<Integer>> levelOrderBottom(TreeNode root) { LinkedList<List<Integer>> ans = new LinkedList<>(); if (root == null) { return ans; } Deque<TreeNode> q = new LinkedList<>(); q.offerLast(root); while (!q.isEmpty()) { List<Integer> t = new ArrayList<>(); for (int i = q.size(); i > 0; --i) { TreeNode node = q.pollFirst(); t.add(node.val); if (node.left != null) { q.offerLast(node.left); } if (node.right != null) { q.offerLast(node.right); } } ans.addFirst(t); } return ans; } }
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
示例 1:
输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-10,null,5] 解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:![]()
示例 2:
输入:nums = [1,3] 输出:[3,1] 解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 104-104 <= nums[i] <= 104nums 按 严格递增 顺序排列方法一:二分 + 递归
我们设计一个递归函数 ,表示当前待构造的二叉搜索树的节点值都在数组 nums 的下标范围 内。该函数返回构造出的二叉搜索树的根节点。
函数 的执行流程如下:
null。答案即为函数 的返回值。
时间复杂度 ,空间复杂度 。其中 为数组 nums 的长度。
class Solution { private int[] nums; public TreeNode sortedArrayToBST(int[] nums) { this.nums = nums; return dfs(0, nums.length - 1); } private TreeNode dfs(int l, int r) { if (l > r) { return null; } int mid = (l + r) >> 1; TreeNode left = dfs(l, mid - 1); TreeNode right = dfs(mid + 1, r); return new TreeNode(nums[mid], left, right); } }
给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。
示例 1:

输入: head = [-10,-3,0,5,9] 输出: [0,-3,9,-10,null,5] 解释: 一个可能的答案是[0,-3,9,-10,null,5],它表示所示的高度平衡的二叉搜索树。
示例 2:
输入: head = [] 输出: []
提示:
head 中的节点数在[0, 2 * 104] 范围内-105 <= Node.val <= 105class Solution { public TreeNode sortedListToBST(ListNode head) { List<Integer> nums = new ArrayList<>(); for (; head != null; head = head.next) { nums.add(head.val); } return buildBST(nums, 0, nums.size() - 1); } private TreeNode buildBST(List<Integer> nums, int start, int end) { if (start > end) { return null; } int mid = (start + end) >> 1; TreeNode root = new TreeNode(nums.get(mid)); root.left = buildBST(nums, start, mid - 1); root.right = buildBST(nums, mid + 1, end); return root; } }
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4] 输出:false
示例 3:
输入:root = [] 输出:true
提示:
[0, 5000] 内-104 <= Node.val <= 104方法一:自底向上的递归
定义函数 计算二叉树的高度,处理逻辑如下:
那么,如果函数 返回的是 ,则说明二叉树 不是平衡二叉树,否则是平衡二叉树。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点数。
class Solution { public boolean isBalanced(TreeNode root) { return height(root) >= 0; } private int height(TreeNode root) { if (root == null) { return 0; } int l = height(root.left); int r = height(root.right); if (l == -1 || r == -1 || Math.abs(l - r) > 1) { return -1; } return 1 + Math.max(l, r); } }
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6] 输出:5
提示:
[0, 105] 内-1000 <= Node.val <= 1000方法一:递归
递归的终止条件是当前节点为空,此时返回 ;如果当前节点左右子树有一个为空,返回不为空的子树的最小深度加 ;如果当前节点左右子树都不为空,返回左右子树最小深度的较小值加 。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
方法二:BFS
使用队列实现广度优先搜索,初始时将根节点加入队列。每次从队列中取出一个节点,如果该节点是叶子节点,则直接返回当前深度;如果该节点不是叶子节点,则将该节点的所有非空子节点加入队列。继续搜索下一层节点,直到找到叶子节点。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { public int minDepth(TreeNode root) { if (root == null) { return 0; } if (root.left == null) { return 1 + minDepth(root.right); } if (root.right == null) { return 1 + minDepth(root.left); } return 1 + Math.min(minDepth(root.left), minDepth(root.right)); } }
class Solution { public int minDepth(TreeNode root) { if (root == null) { return 0; } Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root); int ans = 0; while (true) { ++ans; for (int n = q.size(); n > 0; n--) { TreeNode node = q.poll(); if (node.left == null && node.right == null) { return ans; } if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } } } }
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 输出:true 解释:等于目标和的根节点到叶节点路径如上图所示。
示例 2:
输入:root = [1,2,3], targetSum = 5 输出:false 解释:树中存在两条根节点到叶子节点的路径: (1 --> 2): 和为 3 (1 --> 3): 和为 4 不存在 sum = 5 的根节点到叶子节点的路径。
示例 3:
输入:root = [], targetSum = 0 输出:false 解释:由于树是空的,所以不存在根节点到叶子节点的路径。
提示:
[0, 5000] 内-1000 <= Node.val <= 1000-1000 <= targetSum <= 1000方法一:递归
从根节点开始,递归地对树进行遍历,并在遍历过程中更新节点的值为从根节点到该节点的路径和。当遍历到叶子节点时,判断该路径和是否等于目标值,如果相等则返回 true,否则返回 false。
时间复杂度 ,其中 是二叉树的节点数。对每个节点访问一次。
class Solution { public boolean hasPathSum(TreeNode root, int targetSum) { return dfs(root, targetSum); } private boolean dfs(TreeNode root, int s) { if (root == null) { return false; } s -= root.val; if (root.left == null && root.right == null && s == 0) { return true; } return dfs(root.left, s) || dfs(root.right, s); } }
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 输出:[[5,4,11,2],[5,8,4,5]]
示例 2:
输入:root = [1,2,3], targetSum = 5 输出:[]
示例 3:
输入:root = [1,2], targetSum = 0 输出:[]
提示:
[0, 5000] 内-1000 <= Node.val <= 1000-1000 <= targetSum <= 1000方法一:DFS
我们从根节点开始,递归遍历所有从根节点到叶子节点的路径,并记录路径和。当遍历到叶子节点时,如果此时路径和等于 targetSum,则将此路径加入答案。
时间复杂度 ,其中 是二叉树的节点数。空间复杂度 。
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); public List<List<Integer>> pathSum(TreeNode root, int targetSum) { dfs(root, targetSum); return ans; } private void dfs(TreeNode root, int s) { if (root == null) { return; } s -= root.val; t.add(root.val); if (root.left == null && root.right == null && s == 0) { ans.add(new ArrayList<>(t)); } dfs(root.left, s); dfs(root.right, s); t.remove(t.size() - 1); } }
给你二叉树的根结点 root ,请你将它展开为一个单链表:
TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。示例 1:
输入:root = [1,2,5,3,4,null,6] 输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [0] 输出:[0]
提示:
[0, 2000] 内-100 <= Node.val <= 100进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
方法一:寻找前驱节点
先序遍历的访问顺序是“根、左子树、右子树”,左子树最后一个节点访问完后,接着会访问根节点的右子树节点。
因此,对于当前节点,如果其左子节点不为空,我们找到左子树的最右节点,作为前驱节点,然后将当前节点的右子节点赋给前驱节点的右子节点。然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点置为空。然后将当前节点的右子节点作为下一个节点,继续处理,直至所有节点处理完毕。
时间复杂度 ,空间复杂度 O(1)n$ 是树中节点的个数。
class Solution { public void flatten(TreeNode root) { while (root != null) { if (root.left != null) { // 找到当前节点左子树的最右节点 TreeNode pre = root.left; while (pre.right != null) { pre = pre.right; } // 将左子树的最右节点指向原来的右子树 pre.right = root.right; // 将当前节点指向左子树 root.right = root.left; root.left = null; } root = root.right; } } }
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:s = "rabbbit", t = "rabbit" 输出:3 解释: 如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。 rabbbit rabbbit rabbbit
示例 2:
输入:s = "babgbag", t = "bag" 输出:5 解释: 如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 babgbag babgbag babgbag babgbag babgbag
提示:
1 <= s.length, t.length <= 1000s 和 t 由英文字母组成方法一:动态规划
定义 dp[i][j] 表示 s[0..i] 的子序列中 t[0..j] 出现的个数。初始时 dp[i][0]=1,表示空串是任意字符串的子序列。答案为 dp[m][n]。
当 s[i] == t[j] 时,dp[i][j] = dp[i-1][j-1] + dp[i-1][j],即 s[0..i] 的子序列中 t[0..j] 出现的个数等于 s[0..i-1] 的子序列中 t[0..j-1] 出现的个数加上 s[0..i-1] 的子序列中 t[0..j] 出现的个数。
当 s[i] != t[j] 时,dp[i][j] = dp[i-1][j],即 s[0..i] 的子序列中 t[0..j] 出现的个数等于 s[0..i-1] 的子序列中 t[0..j] 出现的个数。
因此,可以得到状态转移方程:
时间复杂度 ,空间复杂度 。其中 , 分别是字符串 s 和 t 的长度。
class Solution { public int numDistinct(String s, String t) { int m = s.length(), n = t.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; ++i) { dp[i][0] = 1; } for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { dp[i][j] += dp[i - 1][j]; if (s.charAt(i - 1) == t.charAt(j - 1)) { dp[i][j] += dp[i - 1][j - 1]; } } } return dp[m][n]; } }
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例 1:

输入:root = [1,2,3,4,5,6,7] 输出:[1,#,2,3,#,4,5,6,7,#] 解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
示例 2:
输入:root = [] 输出:[]
提示:
[0, 212 - 1] 范围内-1000 <= node.val <= 1000进阶:
方法一:BFS
使用队列进行层序遍历,每次遍历一层时,将当前层的节点按顺序连接起来。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点个数。
方法二:DFS
使用递归进行前序遍历,每次遍历到一个节点时,将其左右子节点按顺序连接起来。
具体地,我们设计一个函数 ,表示将 节点的 指针指向 节点。在函数中,我们首先判断 和 是否为空,若都不为空,则将 指向 ,然后递归地调用 , , 。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点个数。
/* // Definition for a Node. class Node { public int val; public Node left; public Node right; public Node next; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, Node _left, Node _right, Node _next) { val = _val; left = _left; right = _right; next = _next; } }; */ class Solution { public Node connect(Node root) { if (root == null) { return root; } Deque<Node> q = new ArrayDeque<>(); q.offer(root); while (!q.isEmpty()) { Node p = null; for (int n = q.size(); n > 0; --n) { Node node = q.poll(); if (p != null) { p.next = node; } p = node; if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } } return root; } }
/* // Definition for a Node. class Node { public int val; public Node left; public Node right; public Node next; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, Node _left, Node _right, Node _next) { val = _val; left = _left; right = _right; next = _next; } }; */ class Solution { public Node connect(Node root) { if (root != null) { dfs(root.left, root.right); } return root; } private void dfs(Node left, Node right) { if (left == null || right == null) { return; } left.next = right; dfs(left.left, left.right); dfs(left.right, right.left); dfs(right.left, right.right); } }
给定一个二叉树:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL 。
初始状态下,所有 next 指针都被设置为 NULL 。
示例 1:
输入:root = [1,2,3,4,5,null,7] 输出:[1,#,2,3,#,4,5,7,#] 解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
示例 2:
输入:root = [] 输出:[]
提示:
[0, 6000] 内-100 <= Node.val <= 100进阶:
方法一:BFS
使用队列进行层序遍历,每次遍历一层时,将当前层的节点按顺序连接起来。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点个数。
方法二:空间优化
方法一的空间复杂度较高,因为需要使用队列存储每一层的节点。我们可以使用常数空间来实现。
定义两个指针 和 ,分别指向下一层的前一个节点和第一个节点。遍历当前层的节点时,把下一层的节点串起来,同时找到下一层的第一个节点。当前层遍历完后,把下一层的第一个节点 赋值给 ,继续遍历。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点个数。
BFS:
/* // Definition for a Node. class Node { public int val; public Node left; public Node right; public Node next; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, Node _left, Node _right, Node _next) { val = _val; left = _left; right = _right; next = _next; } }; */ class Solution { public Node connect(Node root) { if (root == null) { return root; } Deque<Node> q = new ArrayDeque<>(); q.offer(root); while (!q.isEmpty()) { Node p = null; for (int n = q.size(); n > 0; --n) { Node node = q.poll(); if (p != null) { p.next = node; } p = node; if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } } return root; } }
DFS:
/* // Definition for a Node. class Node { public int val; public Node left; public Node right; public Node next; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, Node _left, Node _right, Node _next) { val = _val; left = _left; right = _right; next = _next; } }; */ class Solution { private Node prev, next; public Node connect(Node root) { Node node = root; while (node != null) { prev = null; next = null; while (node != null) { modify(node.left); modify(node.right); node = node.next; } node = next; } return root; } private void modify(Node curr) { if (curr == null) { return; } if (next == null) { next = curr; } if (prev != null) { prev.next = curr; } prev = curr; } } ```# [118. 杨辉三角](https://leetcode.cn/problems/pascals-triangle) ## 题目描述 <p class="mume-header " id="题目描述-117"></p> <p>给定一个非负整数 <em><code>numRows</code>,</em>生成「杨辉三角」的前 <em><code>numRows</code> </em>行。</p> <p><small>在「杨辉三角」中,每个数是它左上方和右上方的数的和。</small></p> <p><img alt="" src="https://gcore.jsdelivr.net/gh/doocs/leetcode@main/solution/0100-0199/0118.Pascal%27s%20Triangle/images/1626927345-DZmfxB-PascalTriangleAnimated2.gif" /></p> <p><strong>示例 1:</strong></p> <pre> <strong>输入:</strong> numRows = 5 <strong>输出:</strong> [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] </pre> <p><strong>示例 2:</strong></p> <pre> <strong>输入:</strong> numRows = 1 <strong>输出:</strong> [[1]] </pre> <p><strong>提示:</strong></p> <ul> <li><code>1 <= numRows <= 30</code></li> </ul> ## 解法 <p class="mume-header " id="解法-117"></p> 先设置每一行首尾元素为 1,其它元素为 0。然后根据杨辉三角,设置每一行其它元素即可。 ### **Java** <p class="mume-header " id="java-117"></p> ```java class Solution { public List<List<Integer>> generate(int numRows) { List<List<Integer>> ans = new ArrayList<>(); for (int i = 0; i < numRows; ++i) { List<Integer> t = new ArrayList<>(); for (int j = 0; j < i + 1; ++j) { int v = j == 0 || j == i ? 1 : ans.get(i - 1).get(j) + ans.get(i - 1).get(j - 1); t.add(v); } ans.add(t); } return ans; } }
给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。

示例 1:
输入: rowIndex = 3 输出: [1,3,3,1]
示例 2:
输入: rowIndex = 0 输出: [1]
示例 3:
输入: rowIndex = 1 输出: [1,1]
提示:
0 <= rowIndex <= 33进阶:
你可以优化你的算法到 O(rowIndex) 空间复杂度吗?
class Solution { public List<Integer> getRow(int rowIndex) { List<Integer> row = new ArrayList<>(); for (int i = 0; i < rowIndex + 1; ++i) { row.add(1); } for (int i = 2; i < rowIndex + 1; ++i) { for (int j = i - 1; j > 0; --j) { row.set(j, row.get(j) + row.get(j - 1)); } } return row; } }
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]] 输出:-10
提示:
1 <= triangle.length <= 200triangle[0].length == 1triangle[i].length == triangle[i - 1].length + 1-104 <= triangle[i][j] <= 104进阶:
O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?动态规划。自底向上。
空间优化:
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); int[] dp = new int[n + 1]; for (int i = n - 1; i >= 0; --i) { for (int j = 0; j <= i; ++j) { dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j); } } return dp[0]; } }
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 1050 <= prices[i] <= 104方法一:枚举 + 维护前缀最小值
我们可以枚举数组 每个元素作为卖出价格,那么我们需要在前面找到一个最小值作为买入价格,这样才能使得利润最大化。
因此,我们用一个变量 维护数组 的前缀最小值。接下来遍历数组 ,对于每个元素 ,计算其与前面元素的最小值 的差值,更新答案为差值的最大值。然后更新 。继续遍历数组 ,直到遍历结束。
最后返回答案即可。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
class Solution { public int maxProfit(int[] prices) { int ans = 0, mi = prices[0]; for (int v : prices) { ans = Math.max(ans, v - mi); mi = Math.min(mi, v); } return ans; } }
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
提示:
1 <= prices.length <= 3 * 1040 <= prices[i] <= 104方法一:贪心
从第二天开始,如果当天股价大于前一天股价,则在前一天买入,当天卖出,即可获得利润。如果当天股价小于前一天股价,则不买入,不卖出。也即是说,所有上涨交易日都做买卖,所有下跌交易日都不做买卖,最终获得的利润是最大的。
时间复杂度 ,其中 为数组 prices 的长度。空间复杂度 。
方法二:动态规划
我们设 表示第 天交易完后的最大利润,其中 表示当前是否持有股票,持有股票时 ,不持有股票时 。初始状态为 ,其余状态均为 。
如果当前持有股票,那么可能是前一天就持有股票,今天什么都不做,即 ;也可能是前一天不持有股票,今天买入股票,即 。
如果当前不持有股票,那么可能是前一天就不持有股票,今天什么都不做,即 ;也可能是前一天持有股票,今天卖出股票,即 。
因此,我们可以写出状态转移方程:
最终的答案即为 ,其中 为数组 prices 的长度。
时间复杂度 ,空间复杂度 。其中 为数组 prices 的长度。
方法三:动态规划(空间优化)
我们可以发现,在方法二中,第 天的状态,只与第 天的状态有关,因此我们可以只用两个变量来维护第 天的状态,从而将空间复杂度优化到 。
时间复杂度 ,其中 为数组 prices 的长度。空间复杂度 。
贪心
class Solution { public int maxProfit(int[] prices) { int ans = 0; for (int i = 1; i < prices.length; ++i) { ans += Math.max(0, prices[i] - prices[i - 1]); } return ans; } }
动态规划
class Solution { public int maxProfit(int[] prices) { int n = prices.length; int[][] f = new int[n][2]; f[0][0] = -prices[0]; for (int i = 1; i < n; ++i) { f[i][0] = Math.max(f[i - 1][0], f[i - 1][1] - prices[i]); f[i][1] = Math.max(f[i - 1][1], f[i - 1][0] + prices[i]); } return f[n - 1][1]; } }
动态规划(空间优化)
class Solution { public int maxProfit(int[] prices) { int n = prices.length; int[] f = new int[]{-prices[0], 0}; for (int i = 1; i < n; ++i) { int[] g = new int[2]; g[0] = Math.max(f[0], f[1] - prices[i]); g[1] = Math.max(f[1], f[0] + prices[i]); f = g; } return f[1]; } }
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4] 输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。 随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1] 输出:0
提示:
1 <= prices.length <= 1050 <= prices[i] <= 105方法一:动态规划
我们定义以下几个变量,其中:
f1 表示第一次买入股票后的最大利润;f2 表示第一次卖出股票后的最大利润;f3 表示第二次买入股票后的最大利润;f4 表示第二次卖出股票后的最大利润。遍历过程中,直接使用 f1, f2, f3, f4 计算,考虑的是在同一天买入和卖出时,收益是 ,不会对答案产生影响。
最后返回 f2 即可。
时间复杂度 ,空间复杂度 。其中 为数组 prices 的长度。
class Solution { public int maxProfit(int[] prices) { // 第一次买入,第一次卖出,第二次买入,第二次卖出 int f1 = -prices[0], f2 = 0, f3 = -prices[0], f4 = 0; for (int i = 1; i < prices.length; ++i) { f1 = Math.max(f1, -prices[i]); f2 = Math.max(f2, f1 + prices[i]); f3 = Math.max(f3, f2 - prices[i]); f4 = Math.max(f4, f3 + prices[i]); } return f4; } }
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3] 输出:6 解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7] 输出:42 解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
提示:
[1, 3 * 104]-1000 <= Node.val <= 1000思考二叉树递归问题的经典套路:
对于本题,由于要满足题目对 “路径” 的定义,在返回当前子树对外贡献的最大路径和时,需要取 left,right 的最大值
class Solution { private int ans = Integer.MIN_VALUE; public int maxPathSum(TreeNode root) { dfs(root); return ans; } private int dfs(TreeNode node) { if (node == null) { return 0; } int left = Math.max(0, dfs(node.left)); int right = Math.max(0, dfs(node.right)); ans = Math.max(ans, node.val + left + right); return node.val + Math.max(left, right); } }
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
示例 1:
输入: s = "A man, a plan, a canal: Panama" 输出:true 解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car" 输出:false 解释:"raceacar" 不是回文串。
示例 3:
输入:s = " " 输出:true 解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。 由于空字符串正着反着读都一样,所以是回文串。
提示:
1 <= s.length <= 2 * 105s 仅由可打印的 ASCII 字符组成方法一:双指针
我们用双指针 和 分别指向字符串 的两端,接下来循环以下过程,直至 :
false;循环结束,返回 true。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
class Solution { public boolean isPalindrome(String s) { int i = 0, j = s.length() - 1; while (i < j) { if (!Character.isLetterOrDigit(s.charAt(i))) { ++i; } else if (!Character.isLetterOrDigit(s.charAt(j))) { --j; } else if (Character.toLowerCase(s.charAt(i)) != Character.toLowerCase(s.charAt(j))) { return false; } else { ++i; --j; } } return true; } }
按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
si(1 <= i <= k)必须是字典 wordList 中的单词。注意,beginWord 不必是字典 wordList 中的单词。sk == endWord给你两个单词 beginWord 和 endWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回。
示例 1:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]] 解释:存在 2 种最短的转换序列: "hit" -> "hot" -> "dot" -> "dog" -> "cog" "hit" -> "hot" -> "lot" -> "log" -> "cog"
示例 2:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] 输出:[] 解释:endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。
提示:
1 <= beginWord.length <= 5endWord.length == beginWord.length1 <= wordList.length <= 500wordList[i].length == beginWord.lengthbeginWord、endWord 和 wordList[i] 由小写英文字母组成beginWord != endWordwordList 中的所有单词 互不相同DFS。
class Solution { private List<List<String>> ans; private Map<String, Set<String>> prev; public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) { ans = new ArrayList<>(); Set<String> words = new HashSet<>(wordList); if (!words.contains(endWord)) { return ans; } words.remove(beginWord); Map<String, Integer> dist = new HashMap<>(); dist.put(beginWord, 0); prev = new HashMap<>(); Queue<String> q = new ArrayDeque<>(); q.offer(beginWord); boolean found = false; int step = 0; while (!q.isEmpty() && !found) { ++step; for (int i = q.size(); i > 0; --i) { String p = q.poll(); char[] chars = p.toCharArray(); for (int j = 0; j < chars.length; ++j) { char ch = chars[j]; for (char k = 'a'; k <= 'z'; ++k) { chars[j] = k; String t = new String(chars); if (dist.getOrDefault(t, 0) == step) { prev.get(t).add(p); } if (!words.contains(t)) { continue; } prev.computeIfAbsent(t, key -> new HashSet<>()).add(p); words.remove(t); q.offer(t); dist.put(t, step); if (endWord.equals(t)) { found = true; } } chars[j] = ch; } } } if (found) { Deque<String> path = new ArrayDeque<>(); path.add(endWord); dfs(path, beginWord, endWord); } return ans; } private void dfs(Deque<String> path, String beginWord, String cur) { if (cur.equals(beginWord)) { ans.add(new ArrayList<>(path)); return; } for (String precursor : prev.get(cur)) { path.addFirst(precursor); dfs(path, beginWord, precursor); path.removeFirst(); } } }
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk:
1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。sk == endWord给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。
示例 1:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出:5 解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
示例 2:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] 输出:0 解释:endWord "cog" 不在字典中,所以无法进行转换。
提示:
1 <= beginWord.length <= 10endWord.length == beginWord.length1 <= wordList.length <= 5000wordList[i].length == beginWord.lengthbeginWord、endWord 和 wordList[i] 由小写英文字母组成beginWord != endWordwordList 中的所有字符串 互不相同BFS 最小步数模型。本题可以用朴素 BFS,也可以用双向 BFS 优化搜索空间,从而提升效率。
双向 BFS 是 BFS 常见的一个优化方法,主要实现思路如下:
朴素 BFS:
class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { Set<String> words = new HashSet<>(wordList); Queue<String> q = new ArrayDeque<>(); q.offer(beginWord); int ans = 1; while (!q.isEmpty()) { ++ans; for (int i = q.size(); i > 0; --i) { String s = q.poll(); char[] chars = s.toCharArray(); for (int j = 0; j < chars.length; ++j) { char ch = chars[j]; for (char k = 'a'; k <= 'z'; ++k) { chars[j] = k; String t = new String(chars); if (!words.contains(t)) { continue; } if (endWord.equals(t)) { return ans; } q.offer(t); words.remove(t); } chars[j] = ch; } } } return 0; } }
双向 BFS:
class Solution { private Set<String> words; public int ladderLength(String beginWord, String endWord, List<String> wordList) { words = new HashSet<>(wordList); if (!words.contains(endWord)) { return 0; } Queue<String> q1 = new ArrayDeque<>(); Queue<String> q2 = new ArrayDeque<>(); Map<String, Integer> m1 = new HashMap<>(); Map<String, Integer> m2 = new HashMap<>(); q1.offer(beginWord); q2.offer(endWord); m1.put(beginWord, 0); m2.put(endWord, 0); while (!q1.isEmpty() && !q2.isEmpty()) { int t = q1.size() <= q2.size() ? extend(m1, m2, q1) : extend(m2, m1, q2); if (t != -1) { return t + 1; } } return 0; } private int extend(Map<String, Integer> m1, Map<String, Integer> m2, Queue<String> q) { for (int i = q.size(); i > 0; --i) { String s = q.poll(); int step = m1.get(s); char[] chars = s.toCharArray(); for (int j = 0; j < chars.length; ++j) { char ch = chars[j]; for (char k = 'a'; k <= 'z'; ++k) { chars[j] = k; String t = new String(chars); if (!words.contains(t) || m1.containsKey(t)) { continue; } if (m2.containsKey(t)) { return step + 1 + m2.get(t); } q.offer(t); m1.put(t, step + 1); } chars[j] = ch; } } return -1; } } ```# [128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence) ## 题目描述 <p class="mume-header " id="题目描述-127"></p> <p>给定一个未排序的整数数组 <code>nums</code> ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。</p> <p>请你设计并实现时间复杂度为 <code>O(n)</code><em> </em>的算法解决此问题。</p> <p><strong>示例 1:</strong></p> <pre> <strong>输入:</strong>nums = [100,4,200,1,3,2] <strong>输出:</strong>4 <strong>解释:</strong>最长数字连续序列是 <code>[1, 2, 3, 4]。它的长度为 4。</code></pre> <p><strong>示例 2:</strong></p> <pre> <strong>输入:</strong>nums = [0,3,7,2,5,8,4,6,0,1] <strong>输出:</strong>9 </pre> <p><strong>提示:</strong></p> <ul> <li><code>0 <= nums.length <= 10<sup>5</sup></code></li> <li><code>-10<sup>9</sup> <= nums[i] <= 10<sup>9</sup></code></li> </ul> ## 解法 <p class="mume-header " id="解法-127"></p> **哈希表** 我们用哈希表存储数组中的所有元素,然后遍历数组中的每个元素 $x$,如果当前元素的前驱 $x-1$ 不在哈希表中,那么我们以当前元素为起点,不断尝试匹配 $x+1, x+2, x+3, \dots$,直到匹配不到为止,此时的匹配长度即为以 $x$ 为起点的最长连续序列长度,我们更新答案即可。 时间复杂度 $O(n)$,空间复杂度 $O(n)$。其中 $n$ 是数组的长度。 ### **Java** <p class="mume-header " id="java-127"></p> ```java class Solution { public int longestConsecutive(int[] nums) { Set<Integer> s = new HashSet<>(); for (int x : nums) { s.add(x); } int ans = 0; for (int x : nums) { if (!s.contains(x - 1)) { int y = x + 1; while (s.contains(y)) { ++y; } ans = Math.max(ans, y - x); } } return ans; } }
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
1 -> 2 -> 3 表示数字 123 。计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3] 输出:25 解释: 从根到叶子节点路径 1->2 代表数字 12 从根到叶子节点路径 1->3 代表数字 13 因此,数字总和 = 12 + 13 = 25
示例 2:
输入:root = [4,9,0,5,1] 输出:1026 解释: 从根到叶子节点路径 4->9->5 代表数字 495 从根到叶子节点路径 4->9->1 代表数字 491 从根到叶子节点路径 4->0 代表数字 40 因此,数字总和 = 495 + 491 + 40 = 1026
提示:
[1, 1000] 内0 <= Node.val <= 910方法一:DFS
我们可以设计一个函数 ,表示从当前节点 出发,且当前路径数字为 ,返回从当前节点到叶子节点的所有路径数字之和。那么答案就是 。
函数 的计算如下:
时间复杂度 ,空间复杂度 。其中 是二叉树的节点数。
class Solution { public int sumNumbers(TreeNode root) { return dfs(root, 0); } private int dfs(TreeNode root, int s) { if (root == null) { return 0; } s = s * 10 + root.val; if (root.left == null && root.right == null) { return s; } return dfs(root.left, s) + dfs(root.right, s); } }
给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例 1:
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]] 输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]] 解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [["X"]] 输出:[["X"]]
提示:
m == board.lengthn == board[i].length1 <= m, n <= 200board[i][j] 为 'X' 或 'O'DFS、BFS、并查集均可。
DFS:
class Solution { private char[][] board; private int m; private int n; public void solve(char[][] board) { m = board.length; n = board[0].length; this.board = board; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if ((i == 0 || i == m - 1 || j == 0 || j == n - 1) && board[i][j] == 'O') { dfs(i, j); } } } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == '.') { board[i][j] = 'O'; } else if (board[i][j] == 'O') { board[i][j] = 'X'; } } } } private void dfs(int i, int j) { board[i][j] = '.'; int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'O') { dfs(x, y); } } } }
并查集:
class Solution { private int[] p; public void solve(char[][] board) { int m = board.length; int n = board[0].length; p = new int[m * n + 1]; for (int i = 0; i < p.length; ++i) { p[i] = i; } int[] dirs = {-1, 0, 1, 0, -1}; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == 'O') { if (i == 0 || i == m - 1 || j == 0 || j == n - 1) { p[find(i * n + j)] = find(m * n); } else { for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (board[x][y] == 'O') { p[find(x * n + y)] = find(i * n + j); } } } } } } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == 'O' && find(i * n + j) != find(m * n)) { board[i][j] = 'X'; } } } } private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; } }
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a" 输出:[["a"]]
提示:
1 <= s.length <= 16s 仅由小写英文字母组成class Solution { private boolean[][] dp; private List<List<String>> ans; private int n; public List<List<String>> partition(String s) { ans = new ArrayList<>(); n = s.length(); dp = new boolean[n][n]; for (int i = 0; i < n; ++i) { Arrays.fill(dp[i], true); } for (int i = n - 1; i >= 0; --i) { for (int j = i + 1; j < n; ++j) { dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]; } } dfs(s, 0, new ArrayList<>()); return ans; } private void dfs(String s, int i, List<String> t) { if (i == n) { ans.add(new ArrayList<>(t)); return; } for (int j = i; j < n; ++j) { if (dp[i][j]) { t.add(s.substring(i, j + 1)); dfs(s, j + 1, t); t.remove(t.size() - 1); } } } }
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab" 输出:1 解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a" 输出:0
示例 3:
输入:s = "ab" 输出:1
提示:
1 <= s.length <= 2000s 仅由小写英文字母组成两次 dp,dp1[i][j] 表示 i ~ j 的子串是否是回文,可以参考 5. 最长回文子串。dp2[i] 表示以 i 结尾的子串最少需要分割几次,如果本来就是回文串(dp[0][i] == true)就不需要分割,否则枚举分割点 j
class Solution { private boolean[][] g; private int[] f; private String s; private int n; public int minCut(String s) { n = s.length(); g = new boolean[n][n]; for (var e : g) { Arrays.fill(e, true); } for (int i = n - 1; i >= 0; --i) { for (int j = i + 1; j < n; ++j) { g[i][j] = s.charAt(i) == s.charAt(j) && g[i + 1][j - 1]; } } this.s = s; f = new int[n]; Arrays.fill(f, -1); return dfs(0); } private int dfs(int i) { if (i >= n - 1) { return 0; } if (f[i] != -1) { return f[i]; } int ans = Integer.MAX_VALUE; for (int j = i; j < n; ++j) { if (g[i][j]) { ans = Math.min(ans, dfs(j + 1) + (j < n - 1 ? 1 : 0)); } } f[i] = ans; return ans; } }
class Solution { public int minCut(String s) { int n = s.length(); boolean[][] dp1 = new boolean[n][n]; for (int i = n - 1; i >= 0; i--) { for (int j = i; j < n; j++) { dp1[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || dp1[i + 1][j - 1]); } } int[] dp2 = new int[n]; for (int i = 0; i < n; i++) { if (!dp1[0][i]) { dp2[i] = i; for (int j = 1; j <= i; j++) { if (dp1[j][i]) { dp2[i] = Math.min(dp2[i], dp2[j - 1] + 1); } } } } return dp2[n - 1]; } }
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。
class Node {
public int val;
public List<Node> neighbors;
}
测试用例格式:
简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。
邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。
给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。
示例 1:

输入:adjList = [[2,4],[1,3],[2,4],[1,3]] 输出:[[2,4],[1,3],[2,4],[1,3]] 解释: 图中有 4 个节点。 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。
示例 2:

输入:adjList = [[]] 输出:[[]] 解释:输入包含一个空列表。该图仅仅只有一个值为 1 的节点,它没有任何邻居。
示例 3:
输入:adjList = [] 输出:[] 解释:这个图是空的,它不含任何节点。
示例 4:

输入:adjList = [[2],[1]] 输出:[[2],[1]]
提示:
Node.val 都是唯一的,1 <= Node.val <= 100。/* // Definition for a Node. class Node { public int val; public List<Node> neighbors; public Node() { val = 0; neighbors = new ArrayList<Node>(); } public Node(int _val) { val = _val; neighbors = new ArrayList<Node>(); } public Node(int _val, ArrayList<Node> _neighbors) { val = _val; neighbors = _neighbors; } } */ class Solution { private Map<Node, Node> visited = new HashMap<>(); public Node cloneGraph(Node node) { if (node == null) { return null; } if (visited.containsKey(node)) { return visited.get(node); } Node clone = new Node(node.val); visited.put(node, clone); for (Node e : node.neighbors) { clone.neighbors.add(cloneGraph(e)); } return clone; } }
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] 输出: 3 解释: 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3] 输出: -1 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
提示:
gas.length == ncost.length == n1 <= n <= 1050 <= gas[i], cost[i] <= 104方法一:从任意起点开始遍历
我们用 , 分别标记起点和终点,用 表示当前剩余汽油,而 表示当前行驶过的加油站数量。初始时,我们将起点设在最后一个位置,即 。
开始行驶,移动 。若发现当前剩余汽油小于 ,说明当前 作为起点不符合要求,我们将起点 循环左移,并且更新剩余汽油,直至剩余汽油是非负数。
当行驶过的加油站数量达到 时,结束行驶过程。若此时剩余汽油 仍然小于 ,说明不存在这样的起点,返回 。否则,返回起点 。
时间复杂度 ,其中 表示加油站的数量。空间复杂度 。
class Solution { public int canCompleteCircuit(int[] gas, int[] cost) { int n = gas.length; int i = n - 1, j = n - 1; int cnt = 0, s = 0; while (cnt < n) { s += gas[j] - cost[j]; ++cnt; j = (j + 1) % n; while (s < 0 && cnt < n) { --i; s += gas[i] - cost[i]; ++cnt; } } return s < 0 ? -1 : i; } }
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
1 个糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
提示:
n == ratings.length1 <= n <= 2 * 1040 <= ratings[i] <= 2 * 104方法一:两次遍历
我们初始化两个数组 和 ,其中 表示当前孩子比左边孩子评分高时,当前孩子至少应该获得的糖果数,而 表示当前孩子比右边孩子评分高时,当前孩子至少应该获得的糖果数。初始时 , 。
我们从左到右遍历一遍,如果当前孩子比左边孩子评分高,则 ;同理,我们从右到左遍历一遍,如果当前孩子比右边孩子评分高,则 。
最后,我们遍历一遍评分数组,每个孩子至少应该获得的糖果数为 和 中的最大值,将它们累加起来即为答案。
时间复杂度 ,空间复杂度 。其中 是评分数组的长度。
class Solution { public int candy(int[] ratings) { int n = ratings.length; int[] left = new int[n]; int[] right = new int[n]; Arrays.fill(left, 1); Arrays.fill(right, 1); for (int i = 1; i < n; ++i) { if (ratings[i] > ratings[i - 1]) { left[i] = left[i - 1] + 1; } } for (int i = n - 2; i >= 0; --i) { if (ratings[i] > ratings[i + 1]) { right[i] = right[i + 1] + 1; } } int ans = 0; for (int i = 0; i < n; ++i) { ans += Math.max(left[i], right[i]); } return ans; } }
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1] 输出:1
示例 2 :
输入:nums = [4,1,2,1,2] 输出:4
示例 3 :
输入:nums = [1] 输出:1
提示:
1 <= nums.length <= 3 * 104-3 * 104 <= nums[i] <= 3 * 104方法一:位运算
异或运算的性质:
我们对该数组所有元素进行异或运算,结果就是那个只出现一次的数字。
时间复杂度 ,空间复杂度 。其中 是数组 nums 的长度。
class Solution { public int singleNumber(int[] nums) { int ans = 0; for (int v : nums) { ans ^= v; } return ans; } }
class Solution { public int singleNumber(int[] nums) { return Arrays.stream(nums).reduce(0, (a, b) -> a ^ b); } }
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法且不使用额外空间来解决此问题。
示例 1:
输入:nums = [2,2,3,2] 输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,99] 输出:99
提示:
1 <= nums.length <= 3 * 104-231 <= nums[i] <= 231 - 1nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次方法一:位运算
我们可以枚举每个二进制位 ,对于每个二进制位,我们统计所有数字在该二进制位上的和,如果该二进制位上的和能被 整除,那么只出现一次的数字在该二进制位上为 ,否则为 。
时间复杂度 ,空间复杂度 。其中 和 分别是数组的长度和数组中元素的范围。
方法二:数字电路
我们考虑一种更高效的方法,该方法使用数字电路来模拟上述的位运算。
一个整数的每个二进制位是 或 ,只能表示 种状态。但我们需要表示当前遍历过的所有整数的第 位之和模 的结果,因此,我们可以使用 和 两个整数来表示。那么会有以下三种情况:
我们用整数 表示当前要读入的数,那么有以下真值表:
| 新的 | 新的 | |||
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 | 1 |
| 0 | 1 | 0 | 0 | 1 |
| 0 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 0 |
基于以上真值表,我们可以写出逻辑表达式:
以及:
最后结果是 ,因为 的二进制位上为 时表示这个数字出现了 次。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
class Solution { public int singleNumber(int[] nums) { int ans = 0; for (int i = 0; i < 32; i++) { int cnt = 0; for (int num : nums) { cnt += num >> i & 1; } cnt %= 3; ans |= cnt << i; } return ans; } }
class Solution { public int singleNumber(int[] nums) { int a = 0, b = 0; for (int c : nums) { int aa = (~a & b & c) | (a & ~b & ~c); int bb = ~a & (b ^ c); a = aa; b = bb; } return b; } }
需要注意 Golang 中的 int 在 64 位平台上相当于 int64
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:

输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]]
示例 3:

输入:head = [[3,null],[3,0],[3,null]] 输出:[[3,null],[3,0],[3,null]]
提示:
0 <= n <= 1000-104 <= Node.val <= 104Node.random 为 null 或指向链表中的节点。方法一:哈希表
遍历链表,将链表中的每个节点都复制一份,然后将原节点和复制节点的对应关系存储在哈希表中,同时连接好复制节点的 指针。
接下来再遍历链表,根据哈希表中存储的对应关系,将复制节点的 指针连接好。
时间复杂度为 ,空间复杂度为 。其中 为链表的长度。
方法二:拼接 + 拆分
遍历链表,将链表中的每个节点都复制一份,然后将复制节点插入到原节点的后面。
接下来再遍历链表,根据原节点的 指针,将复制节点的 指针连接好。
最后再遍历链表,将链表拆分成原链表和复制链表。
时间复杂度为 ,空间复杂度为 。其中 为链表的长度。
/* // Definition for a Node. class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; } } */ class Solution { public Node copyRandomList(Node head) { Map<Node, Node> d = new HashMap<>(); Node dummy = new Node(0); Node tail = dummy; for (Node cur = head; cur != null; cur = cur.next) { tail.next = new Node(cur.val); tail = tail.next; d.put(cur, tail); } tail = dummy.next; for (Node cur = head; cur != null; cur = cur.next) { tail.random = d.get(cur.random); tail = tail.next; } return dummy.next; } }
/* // Definition for a Node. class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; } } */ class Solution { public Node copyRandomList(Node head) { if (head == null) { return null; } for (Node cur = head; cur != null;) { Node node = new Node(cur.val, cur.next); cur.next = node; cur = node.next; } for (Node cur = head; cur != null; cur = cur.next.next) { if (cur.random != null) { cur.next.random = cur.random.next; } } Node ans = head.next; for (Node cur = head; cur != null;) { Node nxt = cur.next; if (nxt != null) { cur.next = nxt.next; } cur = nxt; } return ans; } }
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。 注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s 和 wordDict[i] 仅有小写英文字母组成wordDict 中的所有字符串 互不相同方法一:动态规划
表示前 个字符组成的字符串 能否拆分成若干个字典中出现的单词。
时间复杂度 。
方法二:前缀树 + 记忆化搜索
根据 构建前缀树 ,然后枚举前缀 作为第一个单词,若在 中,则递归搜索 。
若存在满足条件的拆分方案,返回 。
class Solution { public boolean wordBreak(String s, List<String> wordDict) { Set<String> words = new HashSet<>(wordDict); int n = s.length(); boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 1; i <= n; ++i) { for (int j = 0; j < i; ++j) { if (dp[j] && words.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[n]; } }
class Trie { Trie[] children = new Trie[26]; boolean isEnd; void insert(String w) { Trie node = this; for (char c : w.toCharArray()) { c -= 'a'; if (node.children[c] == null) { node.children[c] = new Trie(); } node = node.children[c]; } node.isEnd = true; } boolean search(String w) { Trie node = this; for (char c : w.toCharArray()) { c -= 'a'; if (node.children[c] == null) { return false; } node = node.children[c]; } return node.isEnd; } } class Solution { private Trie trie = new Trie(); private Map<String, Boolean> memo = new HashMap<>(); public boolean wordBreak(String s, List<String> wordDict) { for (String w : wordDict) { trie.insert(w); } return dfs(s); } private boolean dfs(String s) { if (memo.containsKey(s)) { return memo.get(s); } if ("".equals(s)) { return true; } for (int i = 1; i <= s.length(); ++i) { if (trie.search(s.substring(0, i)) && dfs(s.substring(i))) { memo.put(s, true); return true; } } memo.put(s, false); return false; } }
给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。
注意:词典中的同一个单词可能在分段中被重复使用多次。
示例 1:
输入:s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"] 输出:["cats and dog","cat sand dog"]
示例 2:
输入:s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"] 输出:["pine apple pen apple","pineapple pen apple","pine applepen apple"] 解释: 注意你可以重复使用字典中的单词。
示例 3:
输入:s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] 输出:[]
提示:
1 <= s.length <= 201 <= wordDict.length <= 10001 <= wordDict[i].length <= 10s 和 wordDict[i] 仅有小写英文字母组成wordDict 中所有字符串都 不同方法一:前缀树 + DFS
class Trie { Trie[] children = new Trie[26]; boolean isEnd; void insert(String word) { Trie node = this; for (char c : word.toCharArray()) { c -= 'a'; if (node.children[c] == null) { node.children[c] = new Trie(); } node = node.children[c]; } node.isEnd = true; } boolean search(String word) { Trie node = this; for (char c : word.toCharArray()) { c -= 'a'; if (node.children[c] == null) { return false; } node = node.children[c]; } return node.isEnd; } } class Solution { private Trie trie = new Trie(); public List<String> wordBreak(String s, List<String> wordDict) { for (String w : wordDict) { trie.insert(w); } List<List<String>> res = dfs(s); return res.stream().map(e -> String.join(" ", e)).collect(Collectors.toList()); } private List<List<String>> dfs(String s) { List<List<String>> res = new ArrayList<>(); if ("".equals(s)) { res.add(new ArrayList<>()); return res; } for (int i = 1; i <= s.length(); ++i) { if (trie.search(s.substring(0, i))) { for (List<String> v : dfs(s.substring(i))) { v.add(0, s.substring(0, i)); res.add(v); } } } return res; } }
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:

输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1 输出:false 解释:链表中没有环。
提示:
[0, 104]-105 <= Node.val <= 105pos 为 -1 或者链表中的一个 有效索引 。进阶:你能用 O(1)(即,常量)内存解决此问题吗?
方法一:哈希表
遍历链表,并使用哈希表记录每个节点。当某个节点二次出现时,则表示存在环,直接返回 true。否则链表遍历结束,返回 false。
方法二:快慢指针
定义快慢指针 slow、fast,初始指向 head。
快指针每次走两步,慢指针每次走一步,不断循环。当相遇时,说明链表存在环。如果循环结束依然没有相遇,说明链表不存在环。
public class Solution { public boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return true; } } return false; } }
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:

输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1 输出:返回 null 解释:链表中没有环。
提示:
[0, 104] 内-105 <= Node.val <= 105pos 的值为 -1 或者链表中的一个有效索引进阶:你是否可以使用 O(1) 空间解决此题?
先利用快慢指针判断链表是否有环,没有环则直接返回 null。
若链表有环,我们分析快慢相遇时走过的距离。
对于慢指针(每次走 1 步),走过的距离为 S=X+Y ①;快指针(每次走 2 步)走过的距离为 2S=X+Y+N(Y+Z) ②。如下图所示,其中 N 表示快指针与慢指针相遇时在环中所走过的圈数,而我们要求的环入口,也即是 X 的距离:

我们根据式子 ①②,得出 X+Y=N(Y+Z) => X=(N-1)(Y+Z)+Z。
当 N=1(快指针在环中走了一圈与慢指针相遇) 时,X=(1-1)(Y+Z)+Z,即 X=Z。此时只要定义一个 p 指针指向头节点,然后慢指针与 p 开始同时走,当慢指针与 p 相遇时,也就到达了环入口,直接返回 p 即可。
当 N>1时,也是同样的,说明慢指针除了走 Z 步,还需要绕 N-1 圈才能与 p 相遇。
public class Solution { public ListNode detectCycle(ListNode head) { ListNode slow = head, fast = head; boolean hasCycle = false; while (!hasCycle && fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; hasCycle = slow == fast; } if (!hasCycle) { return null; } ListNode p = head; while (p != slow) { p = p.next; slow = slow.next; } return p; } }
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:

输入:head = [1,2,3,4] 输出:[1,4,2,3]
示例 2:

输入:head = [1,2,3,4,5] 输出:[1,5,2,4,3]
提示:
[1, 5 * 104]1 <= node.val <= 1000先通过快慢指针找到链表中点,将链表划分为左右两部分。之后反转右半部分的链表,然后将左右两个链接依次连接即可。
class Solution { public void reorderList(ListNode head) { if (head == null || head.next == null) { return; } ListNode slow = head; ListNode fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } ListNode cur = slow.next; slow.next = null; ListNode pre = null; while (cur != null) { ListNode t = cur.next; cur.next = pre; pre = cur; cur = t; } cur = head; while (pre != null) { ListNode t = pre.next; pre.next = cur.next; cur.next = pre; cur = pre.next; pre = t; } } }
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3] 输出:[1,2,3]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [1] 输出:[1]
示例 4:
输入:root = [1,2] 输出:[1,2]
示例 5:
输入:root = [1,null,2] 输出:[1,2]
提示:
[0, 100] 内-100 <= Node.val <= 100进阶:递归算法很简单,你可以通过迭代算法完成吗?
1. 递归遍历
先访问根节点,接着递归左子树、右子树。
2. 栈实现非递归遍历
非递归的思路如下:
3. Morris 实现前序遍历
Morris 遍历无需使用栈,空间复杂度为 O(1)。核心思想是:
遍历二叉树节点,
root.rightroot.left。root.right。递归:
class Solution { private List<Integer> ans; public List<Integer> preorderTraversal(TreeNode root) { ans = new ArrayList<>(); dfs(root); return ans; } private void dfs(TreeNode root) { if (root == null) { return; } ans.add(root.val); dfs(root.left); dfs(root.right); } }
非递归:
class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<TreeNode> stk = new ArrayDeque<>(); stk.push(root); while (!stk.isEmpty()) { TreeNode node = stk.pop(); ans.add(node.val); if (node.right != null) { stk.push(node.right); } if (node.left != null) { stk.push(node.left); } } return ans; } }
Morris 遍历:
class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); while (root != null) { if (root.left == null) { ans.add(root.val); root = root.right; } else { TreeNode prev = root.left; while (prev.right != null && prev.right != root) { prev = prev.right; } if (prev.right == null) { ans.add(root.val); prev.right = root; root = root.left; } else { prev.right = null; root = root.right; } } } return ans; } }
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
示例 1:
输入:root = [1,null,2,3] 输出:[3,2,1]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [1] 输出:[1]
提示:
[0, 100] 内-100 <= Node.val <= 100进阶:递归算法很简单,你可以通过迭代算法完成吗?
1. 递归遍历
先递归左右子树,再访问根节点。
2. 栈实现非递归遍历
非递归的思路如下:
先序遍历的顺序是:头、左、右,如果我们改变左右孩子的顺序,就能将顺序变成:头、右、左。
我们先不打印头节点,而是存放到另一个收集栈 ans 中,最后遍历结束,输出收集栈元素,即是后序遍历:左、右、头。收集栈是为了实现结果列表的逆序。我们也可以直接使用链表,每次插入元素时,放在头部,最后直接返回链表即可,无需进行逆序。
3. Morris 实现后序遍历
Morris 遍历无需使用栈,空间复杂度为 O(1)。核心思想是:
遍历二叉树节点,
root.leftroot.right。root.left。Morris 后序遍历跟 Morris 前序遍历思路一致,只是将前序的“根左右”变为“根右左”,最后逆序结果即可变成“左右根”。
递归:
class Solution { private List<Integer> ans; public List<Integer> postorderTraversal(TreeNode root) { ans = new ArrayList<>(); dfs(root); return ans; } private void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); dfs(root.right); ans.add(root.val); } }
栈实现非递归:
class Solution { public List<Integer> postorderTraversal(TreeNode root) { LinkedList<Integer> ans = new LinkedList<>(); if (root == null) { return ans; } Deque<TreeNode> stk = new ArrayDeque<>(); stk.push(root); while (!stk.isEmpty()) { TreeNode node = stk.pop(); ans.addFirst(node.val); if (node.left != null) { stk.push(node.left); } if (node.right != null) { stk.push(node.right); } } return ans; } }
Morris 遍历:
class Solution { public List<Integer> postorderTraversal(TreeNode root) { LinkedList<Integer> ans = new LinkedList<>(); while (root != null) { if (root.right == null) { ans.addFirst(root.val); root = root.left; } else { TreeNode next = root.right; while (next.left != null && next.left != root) { next = next.left; } if (next.left == null) { ans.addFirst(root.val); next.left = root; root = root.right; } else { next.left = null; root = root.left; } } } return ans; } }
LRUCache 类:LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
1 <= capacity <= 30000 <= key <= 100000 <= value <= 1052 * 105 次 get 和 put方法一:哈希表 + 双向链表
“哈希表 + 双向链表”实现。其中:
对于 get 操作,判断 key 是否存在哈希表中:
对于 put 操作,同样先判断 key 是否存在哈希表中:
双向链表节点(哈希表的 value)的结构如下:
class Node { int key; int value; Node prev; Node next; Node() { } Node(int key, int value) { this.key = key; this.value = value; } }
你可能会问,哈希表的 value 为何还要存放 key?
这是因为,双向链表有一个删除尾节点的操作。我们定位到双向链表的尾节点,在链表中删除之后,还要找到该尾节点在哈希表中的位置,因此需要根据 value 中存放的 key,定位到哈希表的数据项,然后将其删除。
class Node { int key; int val; Node prev; Node next; Node() { } Node(int key, int val) { this.key = key; this.val = val; } } class LRUCache { private Map<Integer, Node> cache = new HashMap<>(); private Node head = new Node(); private Node tail = new Node(); private int capacity; private int size; public LRUCache(int capacity) { this.capacity = capacity; head.next = tail; tail.prev = head; } public int get(int key) { if (!cache.containsKey(key)) { return -1; } Node node = cache.get(key); moveToHead(node); return node.val; } public void put(int key, int value) { if (cache.containsKey(key)) { Node node = cache.get(key); node.val = value; moveToHead(node); } else { Node node = new Node(key, value); cache.put(key, node); addToHead(node); ++size; if (size > capacity) { node = removeTail(); cache.remove(node.key); --size; } } } private void moveToHead(Node node) { removeNode(node); addToHead(node); } private void removeNode(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } private void addToHead(Node node) { node.next = head.next; node.prev = head; head.next = node; node.next.prev = node; } private Node removeTail() { Node node = tail.prev; removeNode(node); return node; } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */
给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
插入排序 算法的步骤:
下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。
对链表进行插入排序。

示例 1:

输入: head = [4,2,1,3] 输出: [1,2,3,4]
示例 2:

输入: head = [-1,5,3,4,0] 输出: [-1,0,3,4,5]
提示:
[1, 5000]范围内-5000 <= Node.val <= 5000遍历链表,每次将遍历到的结点 cur 与前一个结点 pre 进行值比较:
依次遍历,直至 cur 指向空,遍历结束。
class Solution { public ListNode insertionSortList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode dummy = new ListNode(head.val, head); ListNode pre = dummy, cur = head; while (cur != null) { if (pre.val <= cur.val) { pre = cur; cur = cur.next; continue; } ListNode p = dummy; while (p.next.val <= cur.val) { p = p.next; } ListNode t = cur.next; cur.next = p.next; p.next = cur; pre.next = t; cur = t; } return dummy.next; } }
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3] 输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0] 输出:[-1,0,3,4,5]
示例 3:
输入:head = [] 输出:[]
提示:
[0, 5 * 104] 内-105 <= Node.val <= 105进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
先用快慢指针找到链表中点,然后分成左右两个链表,递归排序左右链表。最后合并两个排序的链表即可。
class Solution { public ListNode sortList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode slow = head, fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } ListNode t = slow.next; slow.next = null; ListNode l1 = sortList(head); ListNode l2 = sortList(t); ListNode dummy = new ListNode(); ListNode cur = dummy; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; } cur = cur.next; } cur.next = l1 == null ? l2 : l1; return dummy.next; } }
给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
示例 1:
输入:points = [[1,1],[2,2],[3,3]] 输出:3
示例 2:
输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]] 输出:4
提示:
1 <= points.length <= 300points[i].length == 2-104 <= xi, yi <= 104points 中的所有点 互不相同方法一:暴力枚举
我们可以枚举任意两个点 ,把这两个点连成一条直线,那么此时这条直线上的点的个数就是 2,接下来我们再枚举其他点 ,判断它们是否在同一条直线上,如果在,那么直线上的点的个数就加 1,如果不在,那么直线上的点的个数不变。找出所有直线上的点的个数的最大值,即为答案。
时间复杂度 ,空间复杂度 。其中 是数组 points 的长度。
方法二:枚举 + 哈希表
我们可以枚举一个点 ,把其他所有点 与 连成的直线的斜率存入哈希表中,斜率相同的点在同一条直线上,哈希表的键为斜率,值为直线上的点的个数。找出哈希表中的最大值,即为答案。为了避免精度问题,我们可以将斜率 进行约分,约分的方法是求最大公约数,然后分子分母同时除以最大公约数,将求得的分子分母作为哈希表的键。
时间复杂度 ,空间复杂度 。其中 和 分别是数组 points 的长度和数组 points 所有横纵坐标差的最大值。
相似题目:
class Solution { public int maxPoints(int[][] points) { int n = points.length; int ans = 1; for (int i = 0; i < n; ++i) { int x1 = points[i][0], y1 = points[i][1]; for (int j = i + 1; j < n; ++j) { int x2 = points[j][0], y2 = points[j][1]; int cnt = 2; for (int k = j + 1; k < n; ++k) { int x3 = points[k][0], y3 = points[k][1]; int a = (y2 - y1) * (x3 - x1); int b = (y3 - y1) * (x2 - x1); if (a == b) { ++cnt; } } ans = Math.max(ans, cnt); } } return ans; } }
class Solution { public int maxPoints(int[][] points) { int n = points.length; int ans = 1; for (int i = 0; i < n; ++i) { int x1 = points[i][0], y1 = points[i][1]; Map<String, Integer> cnt = new HashMap<>(); for (int j = i + 1; j < n; ++j) { int x2 = points[j][0], y2 = points[j][1]; int dx = x2 - x1, dy = y2 - y1; int g = gcd(dx, dy); String k = (dx / g) + "." + (dy / g); cnt.put(k, cnt.getOrDefault(k, 0) + 1); ans = Math.max(ans, cnt.get(k) + 1); } } return ans; } private int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } }
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
'+'、'-'、'*' 和 '/' 。示例 1:
输入:tokens = ["2","1","+","3","*"] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22
提示:
1 <= tokens.length <= 104tokens[i] 是一个算符("+"、"-"、"*" 或 "/"),或是在范围 [-200, 200] 内的一个整数逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
( 1 + 2 ) * ( 3 + 4 ) 。( ( 1 2 + ) ( 3 4 + ) * ) 。逆波兰表达式主要有以下两个优点:
1 2 + 3 4 + * 也可以依据次序计算出正确结果。利用栈存储运算数,每次遇到符号,对栈顶两个元素进行运算。
需要注意 Python 的整除对负数也是向下取整(例如:6 // -132 = -1),和答案对应不上,所以需要特殊处理。
class Solution { public int evalRPN(String[] tokens) { Deque<Integer> stk = new ArrayDeque<>(); for (String t : tokens) { if (t.length() > 1 || Character.isDigit(t.charAt(0))) { stk.push(Integer.parseInt(t)); } else { int y = stk.pop(); int x = stk.pop(); switch (t) { case "+": stk.push(x + y); break; case "-": stk.push(x - y); break; case "*": stk.push(x * y); break; default: stk.push(x / y); break; } } } return stk.pop(); } }
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue" 输出:"blue is sky the"
示例 2:
输入:s = " hello world " 输出:"world hello" 解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example" 输出:"example good a" 解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示:
1 <= s.length <= 104s 包含英文大小写字母、数字和空格 ' 's 中 至少存在一个 单词进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1) 额外空间复杂度的 原地 解法。
方法一:使用语言自带的函数
我们将字符串按照空格分割成字符串列表,然后将列表反转,最后将列表拼接成以空格分割的字符串即可。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
方法二:双指针
我们可以使用双指针 和 ,每次找到一个单词,将其添加到结果列表中,最后将结果列表反转,再拼接成字符串即可。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
class Solution { public String reverseWords(String s) { List<String> words = Arrays.asList(s.trim().split("\\s+")); Collections.reverse(words); return String.join(" ", words); } }
class Solution { public String reverseWords(String s) { List<String> words = new ArrayList<>(); int n = s.length(); for (int i = 0; i < n;) { while (i < n && s.charAt(i) == ' ') { ++i; } if (i < n) { StringBuilder t = new StringBuilder(); int j = i; while (j < n && s.charAt(j) != ' ') { t.append(s.charAt(j++)); } words.add(t.toString()); i = j; } } Collections.reverse(words); return String.join(" ", words); } }
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
示例 1:
输入: nums = [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
1 <= nums.length <= 2 * 104-10 <= nums[i] <= 10nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数考虑当前位置 i:
因此,分别维护 fmax 和 fmin。
fmax(i) = max(nums[i], fmax(i - 1) * nums[i], fmin(i - 1) * nums[i])fmin(i) = min(nums[i], fmax(i - 1) * nums[i], fmin(i - 1) * nums[i])res = max(fmax(i)), i∈[0, n)class Solution { public int maxProduct(int[] nums) { int maxf = nums[0], minf = nums[0], res = nums[0]; for (int i = 1; i < nums.length; ++i) { int m = maxf, n = minf; maxf = Math.max(nums[i], Math.max(m * nums[i], n * nums[i])); minf = Math.min(nums[i], Math.min(m * nums[i], n * nums[i])); res = Math.max(res, maxf); } return res; } }
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
4 次,则可以得到 [4,5,6,7,0,1,2]7 次,则可以得到 [0,1,2,4,5,6,7]注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length1 <= n <= 5000-5000 <= nums[i] <= 5000nums 中的所有整数 互不相同nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转方法一:二分查找
初始,判断数组首尾元素的大小关系,若 nums[0] <= nums[n - 1] 条件成立,则说明当前数组已经是递增数组,最小值一定是数组第一个元素,提前返回 nums[0]。
否则,进行二分判断。若 nums[0] <= nums[mid],说明 [left, mid] 范围内的元素构成递增数组,最小值一定在 mid 的右侧,否则说明 [mid + 1, right] 范围内的元素构成递增数组,最小值一定在 mid 的左侧。
除了 nums[0],也可以以 nums[right] 作为参照物,若 nums[mid] < nums[right] 成立,则最小值存在于 [left, mid] 范围当中,否则存在于 [mid + 1, right]。
时间复杂度:
class Solution { public int findMin(int[] nums) { int n = nums.length; if (nums[0] <= nums[n - 1]) { return nums[0]; } int left = 0, right = n - 1; while (left < right) { int mid = (left + right) >> 1; if (nums[0] <= nums[mid]) { left = mid + 1; } else { right = mid; } } return nums[left]; } }
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
4 次,则可以得到 [4,5,6,7,0,1,4]7 次,则可以得到 [0,1,4,4,5,6,7]注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5] 输出:1
示例 2:
输入:nums = [2,2,2,0,1] 输出:0
提示:
n == nums.length1 <= n <= 5000-5000 <= nums[i] <= 5000nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转进阶:这道题与 寻找旋转排序数组中的最小值 类似,但 nums 可能包含重复元素。允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
方法一:二分查找
若 nums[mid] > nums[right],说明最小值在 mid 的右边;若 nums[mid] < nums[right],说明最小值在 mid 的左边(包括 mid);若相等,无法判断,直接将 right 减 1。循环比较。
最后返回 nums[left] 即可。
时间复杂度 O(logn)。
class Solution { public int findMin(int[] nums) { int left = 0, right = nums.length - 1; while (left < right) { int mid = (left + right) >> 1; if (nums[mid] > nums[right]) { left = mid + 1; } else if (nums[mid] < nums[right]) { right = mid; } else { --right; } } return nums[left]; } }
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。int getMin() 获取堆栈中的最小元素。示例 1:
输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] 输出: [null,null,null,null,-3,null,0,-2] 解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.
提示:
-231 <= val <= 231 - 1pop、top 和 getMin 操作总是在 非空栈 上调用push, pop, top, and getMin最多被调用 3 * 104 次方法一:双栈
我们用两个栈来实现,其中 stk1 用来存储数据,stk2 用来存储当前栈中的最小值。初始时,stk2 中存储一个极大值。
stk1,并将 min(x, stk2[-1]) 压入 stk2。stk1 和 stk2 的栈顶元素都弹出。stk1 的栈顶元素即可。stk2 的栈顶元素即可。每个操作的时间复杂度为 。整体的空间复杂度为 ,其中 为栈中元素的个数。
class MinStack { private Deque<Integer> stk1 = new ArrayDeque<>(); private Deque<Integer> stk2 = new ArrayDeque<>(); public MinStack() { stk2.push(Integer.MAX_VALUE); } public void push(int val) { stk1.push(val); stk2.push(Math.min(val, stk2.peek())); } public void pop() { stk1.pop(); stk2.pop(); } public int top() { return stk1.peek(); } public int getMin() { return stk2.peek(); } }
给你一个二叉树的根节点 root ,请你将此二叉树上下翻转,并返回新的根节点。
你可以按下面的步骤翻转一棵二叉树:
上面的步骤逐层进行。题目数据保证每个右节点都有一个同级节点(即共享同一父节点的左节点)且不存在子节点。
示例 1:
输入:root = [1,2,3,4,5] 输出:[4,5,2,null,null,3,1]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [1] 输出:[1]
提示:
[0, 10] 内1 <= Node.val <= 10方法一:递归
若根节点为空,或者根节点左子树为空,直接返回根节点。
递归处理左子树,返回的根节点 newRoot,也就是二叉树上下翻转后的根节点。
然后处理根节点 root,根节点变成左子节点的右子节点,而根节点的右子节点变成左子节点的左子节点。
接着将根节点 root 的左右子节点置为空,最后返回 newRoot 即可。
时间复杂度 ,空间复杂度 。其中 为二叉树节点个数。
class Solution { public TreeNode upsideDownBinaryTree(TreeNode root) { if (root == null || root.left == null) { return root; } TreeNode newRoot = upsideDownBinaryTree(root.left); root.left.right = root; root.left.left = root.right; root.left = null; root.right = null; return newRoot; } }
给你一个文件,并且该文件只能通过给定的 read4 方法来读取,请实现一个方法使其能够读取 n 个字符。
read4 方法:
API read4 可以从文件中读取 4 个连续的字符,并且将它们写入缓存数组 buf 中。
返回值为实际读取的字符个数。
注意 read4() 自身拥有文件指针,很类似于 C 语言中的 FILE *fp 。
read4 的定义:
参数类型: char[] buf4 返回类型: int 注意: buf4[] 是目标缓存区不是源缓存区,read4 的返回结果将会复制到 buf4[] 当中。
下列是一些使用 read4 的例子:
File file("abcde"); // 文件名为 "abcde", 初始文件指针 (fp) 指向 'a'
char[] buf4 = new char[4]; // 创建一个缓存区使其能容纳足够的字符
read4(buf4); // read4 返回 4。现在 buf4 = "abcd",fp 指向 'e'
read4(buf4); // read4 返回 1。现在 buf4 = "e",fp 指向文件末尾
read4(buf4); // read4 返回 0。现在 buf = "",fp 指向文件末尾
read 方法:
通过使用 read4 方法,实现 read 方法。该方法可以从文件中读取 n 个字符并将其存储到缓存数组 buf 中。您 不能 直接操作文件。
返回值为实际读取的字符。
read 的定义:
参数类型: char[] buf, int n 返回类型: int 注意: buf[] 是目标缓存区不是源缓存区,你需要将结果写入 buf[] 中。
示例 1:
输入: file = "abc", n = 4 输出: 3 解释: 当执行你的 read 方法后,buf 需要包含 "abc"。 文件一共 3 个字符,因此返回 3。 注意 "abc" 是文件的内容,不是 buf 的内容,buf 是你需要写入结果的目标缓存区。
示例 2:
输入: file = "abcde", n = 5 输出: 5 解释: 当执行你的 read 方法后,buf 需要包含 "abcde"。文件共 5 个字符,因此返回 5。
示例 3:
输入: file = "abcdABCD1234", n = 12 输出: 12 解释: 当执行你的 read 方法后,buf 需要包含 "abcdABCD1234"。文件一共 12 个字符,因此返回 12。
示例 4:
输入: file = "leetcode", n = 5 输出: 5 解释: 当执行你的 read 方法后,buf 需要包含 "leetc"。文件中一共 5 个字符,因此返回 5。
提示:
read4 获取而 不能 通过 read。read 函数只在每个测试用例调用一次。buf 保证有足够的空间存下 n 个字符。 方法一:模拟
直接模拟读取文件的过程,每次读取 4 个字符,然后将读取的字符存入缓存数组中,直到读取的字符数目达到 n 或者文件读取完毕。
时间复杂度 。其中 为要读取的字符数目。
/** * The read4 API is defined in the parent class Reader4. * int read4(char[] buf4); */ public class Solution extends Reader4 { /** * @param buf Destination buffer * @param n Number of characters to read * @return The number of actual characters read */ public int read(char[] buf, int n) { char[] buf4 = new char[4]; int i = 0, v = 5; while (v >= 4) { v = read4(buf4); for (int j = 0; j < v; ++j) { buf[i++] = buf4[j]; if (i >= n) { return n; } } } return i; } }
给你一个文件 file ,并且该文件只能通过给定的 read4 方法来读取,请实现一个方法使其能够使 read 读取 n 个字符。注意:你的 read 方法可能会被调用多次。
read4 的定义:
read4 API 从文件中读取 4 个连续的字符,然后将这些字符写入缓冲区数组 buf4 。
返回值是读取的实际字符数。
请注意,read4() 有其自己的文件指针,类似于 C 中的 FILE * fp 。
参数类型: char[] buf4
返回类型: int
注意: buf4[] 是目标缓存区不是源缓存区,read4 的返回结果将会复制到 buf4[] 当中。
下列是一些使用 read4 的例子:

File file("abcde"); // 文件名为 "abcde", 初始文件指针 (fp) 指向 'a'
char[] buf4 = new char[4]; // 创建一个缓存区使其能容纳足够的字符
read4(buf4); // read4 返回 4。现在 buf4 = "abcd",fp 指向 'e'
read4(buf4); // read4 返回 1。现在 buf4 = "e",fp 指向文件末尾
read4(buf4); // read4 返回 0。现在 buf4 = "",fp 指向文件末尾
read 方法:
通过使用 read4 方法,实现 read 方法。该方法可以从文件中读取 n 个字符并将其存储到缓存数组 buf 中。您 不能 直接操作 file 。
返回值为实际读取的字符。
read 的定义:
参数类型: char[] buf, int n
返回类型: int
注意: buf[] 是目标缓存区不是源缓存区,你需要将结果写入 buf[] 中。
注意:
read4 获取而 不能 通过 read。read 函数可以被调用 多次。buf 保证有足够的空间存下 n 个字符。 read 函数使用的是同一个 buf。示例 1:
输入: file = "abc", queries = [1,2,1]
输出:[1,2,0]
解释:测试用例表示以下场景:
File file("abc");
Solution sol;
sol.read (buf, 1); // 调用 read 方法后,buf 应该包含 “a”。我们从文件中总共读取了 1 个字符,所以返回 1。
sol.read (buf, 2); // 现在 buf 应该包含 "bc"。我们从文件中总共读取了 2 个字符,所以返回 2。
sol.read (buf, 1); // 我们已经到达文件的末尾,不能读取更多的字符。所以返回 0。
假设已经分配了 buf ,并保证有足够的空间存储文件中的所有字符。
示例 2:
输入:file = "abc", queries = [4,1]
输出:[3,0]
解释:测试用例表示以下场景:
File file("abc");
Solution sol;
sol.read (buf, 4); // 调用 read 方法后,buf 应该包含 “abc”。我们从文件中总共读取了 3 个字符,所以返回 3。
sol.read (buf, 1); // 我们已经到达文件的末尾,不能读取更多的字符。所以返回 0。
提示:
1 <= file.length <= 500file 由英语字母和数字组成1 <= queries.length <= 101 <= queries[i] <= 500方法一:模拟
/** * The read4 API is defined in the parent class Reader4. * int read4(char[] buf4); */ public class Solution extends Reader4 { private char[] buf4 = new char[4]; private int i; private int size; /** * @param buf Destination buffer * @param n Number of characters to read * @return The number of actual characters read */ public int read(char[] buf, int n) { int j = 0; while (j < n) { if (i == size) { size = read4(buf4); i = 0; if (size == 0) { break; } } while (j < n && i < size) { buf[j++] = buf4[i++]; } } return j; } }
给你一个字符串 s ,请你找出 至多 包含 两个不同字符 的最长子串,并返回该子串的长度。
示例 1:
输入:s = "eceba" 输出:3 解释:满足题目要求的子串是 "ece" ,长度为 3 。
示例 2:
输入:s = "ccaabbb" 输出:5 解释:满足题目要求的子串是 "aabbb" ,长度为 5 。
提示:
1 <= s.length <= 105s 由英文字母组成方法一:哈希表 + 滑动窗口
我们维护一个哈希表 cnt 记录当前滑动窗口中各个字符出现的次数,如果哈希表中的键值对个数超过 ,则说明当前滑动窗口中包含了超过 个不同的字符,此时需要移动左指针 j,直到哈希表中的键值对个数不超过 为止,然后更新窗口的最大长度。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
class Solution { public int lengthOfLongestSubstringTwoDistinct(String s) { Map<Character, Integer> cnt = new HashMap<>(); int n = s.length(); int ans = 0; for (int i = 0, j = 0; i < n; ++i) { char c = s.charAt(i); cnt.put(c, cnt.getOrDefault(c, 0) + 1); while (cnt.size() > 2) { char t = s.charAt(j++); cnt.put(t, cnt.get(t) - 1); if (cnt.get(t) == 0) { cnt.remove(t); } } ans = Math.max(ans, i - j + 1); } return ans; } }
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0listA - 第一个链表listB - 第二个链表skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
提示:
listA 中节点数目为 mlistB 中节点数目为 n1 <= m, n <= 3 * 1041 <= Node.val <= 1050 <= skipA <= m0 <= skipB <= nlistA 和 listB 没有交点,intersectVal 为 0listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]进阶:你能否设计一个时间复杂度 O(m + n) 、仅用 O(1) 内存的解决方案?
方法一:双指针
使用两个指针 , 分别指向两个链表 , 。
同时遍历链表,当 到达链表 的末尾时,重新定位到链表 的头节点;当 到达链表 的末尾时,重新定位到链表 的头节点。
若两指针相遇,所指向的结点就是第一个公共节点。若没相遇,说明两链表无公共节点,此时两个指针都指向 。
时间复杂度 ,其中 和 分别是链表 和 的长度。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode a = headA, b = headB; while (a != b) { a = a == null ? headB : a.next; b = b == null ? headA : b.next; } return a; } }
给定两个字符串 s 和 t ,如果它们的编辑距离为 1 ,则返回 true ,否则返回 false 。
字符串 s 和字符串 t 之间满足编辑距离等于 1 有三种可能的情形:
s 中插入 恰好一个 字符得到 ts 中删除 恰好一个 字符得到 ts 中用 一个不同的字符 替换 恰好一个 字符得到 t示例 1:
输入: s = "ab", t = "acb" 输出: true 解释: 可以将 'c' 插入字符串 s 来得到 t。
示例 2:
输入: s = "cab", t = "ad" 输出: false 解释: 无法通过 1 步操作使 s 变为 t。
提示:
0 <= s.length, t.length <= 104s 和 t 由小写字母,大写字母和数字组成方法一:分情况讨论
记 表示字符串 的长度, 表示字符串 的长度。我们可以假定 恒大于等于 。
若 ,直接返回 false;
否则,遍历 和 ,若遇到 不等于 :
遍历结束,说明遍历过的 跟 所有字符相等,此时需要满足 。
时间复杂度 ,空间复杂度 。
class Solution { public boolean isOneEditDistance(String s, String t) { int m = s.length(), n = t.length(); if (m < n) { return isOneEditDistance(t, s); } if (m - n > 1) { return false; } for (int i = 0; i < n; ++i) { if (s.charAt(i) != t.charAt(i)) { if (m == n) { return s.substring(i + 1).equals(t.substring(i + 1)); } return s.substring(i + 1).equals(t.substring(i)); } } return m == n + 1; } }
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
示例 1:
输入:nums = [1,2,3,1] 输出:2 解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4] 输出:1 或 5 解释:你的函数可以返回索引 1,其峰值元素为 2; 或者返回索引 5, 其峰值元素为 6。
提示:
1 <= nums.length <= 1000-231 <= nums[i] <= 231 - 1i 都有 nums[i] != nums[i + 1]二分查找。
class Solution { public int findPeakElement(int[] nums) { int left = 0, right = nums.length - 1; while (left < right) { int mid = (left + right) >> 1; if (nums[mid] > nums[mid + 1]) { right = mid; } else { left = mid + 1; } } return left; } }
给你一个闭区间 [lower, upper] 和一个 按从小到大排序 的整数数组 nums ,其中元素的范围在闭区间 [lower, upper] 当中。
如果一个数字 x 在 [lower, upper] 区间内,并且 x 不在 nums 中,则认为 x 缺失。
返回 准确涵盖所有缺失数字 的 最小排序 区间列表。也就是说,nums 的任何元素都不在任何区间内,并且每个缺失的数字都在其中一个区间内。
示例 1:
输入: nums = [0, 1, 3, 50, 75], lower = 0 , upper = 99 输出: [[2,2],[4,49],[51,74],[76,99]] 解释:返回的区间是: [2,2] [4,49] [51,74] [76,99]
示例 2:
输入: nums = [-1], lower = -1, upper = -1 输出: [] 解释: 没有缺失的区间,因为没有缺失的数字。
提示:
-109 <= lower <= upper <= 1090 <= nums.length <= 100lower <= nums[i] <= uppernums 中的所有值 互不相同方法一:模拟
按照题意模拟即可。
时间复杂度 ,忽略答案的空间消耗,空间复杂度 。其中 为数组 nums 的长度。
class Solution { public List<String> findMissingRanges(int[] nums, int lower, int upper) { int n = nums.length; List<String> ans = new ArrayList<>(); if (n == 0) { ans.add(f(lower, upper)); return ans; } if (nums[0] > lower) { ans.add(f(lower, nums[0] - 1)); } for (int i = 1; i < n; ++i) { int a = nums[i - 1], b = nums[i]; if (b - a > 1) { ans.add(f(a + 1, b - 1)); } } if (nums[n - 1] < upper) { ans.add(f(nums[n - 1] + 1, upper)); } return ans; } private String f(int a, int b) { return a == b ? a + "" : a + "->" + b; } }
给定一个无序的数组 nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0 。
您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。
示例 1:
输入: nums = [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
示例 2:
输入: nums = [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。
提示:
1 <= nums.length <= 1050 <= nums[i] <= 109前言
一种容易想到的解法是将数组排序后得到相邻元素之间最大的差值,时间复杂度 ,不符合题目要求。
只有使用不基于比较的的排序算法才能在线性时间复杂度解决。
方法一:桶排序
假设数组 有 个元素,所有元素从小到大依次是 到 ,最大间距是 。考虑数组中的最大元素和最小元素之差。
因此 ,即最大间距至少为 。
可以利用桶排序的思想,设定桶的大小(即每个桶最多包含的不同元素个数)为 ,将元素按照元素值均匀分布到各个桶内,则同一个桶内的任意两个元素之差小于 ,差为 的两个元素一定在两个不同的桶内。对于每个桶,维护桶内的最小值和最大值,初始时每个桶内的最小值和最大值分别是正无穷和负无穷,表示桶内没有元素。
遍历数组 中的所有元素。对于每个元素,根据该元素与最小元素之差以及桶的大小计算该元素应该分到的桶的编号,可以确保编号小的桶内的元素都小于编号大的桶内的元素,使用元素值更新元素所在的桶内的最小值和最大值。
遍历数组结束之后,每个非空的桶内的最小值和最大值都可以确定。按照桶的编号从小到大的顺序依次遍历每个桶,当前的桶的最小值和上一个非空的桶的最大值是排序后的相邻元素,计算两个相邻元素之差,并更新最大间距。遍历桶结束之后即可得到最大间距。
时间复杂度 ,空间复杂度 。
class Solution { public int maximumGap(int[] nums) { int n = nums.length; if (n < 2) { return 0; } int inf = 0x3f3f3f3f; int mi = inf, mx = -inf; for (int v : nums) { mi = Math.min(mi, v); mx = Math.max(mx, v); } int bucketSize = Math.max(1, (mx - mi) / (n - 1)); int bucketCount = (mx - mi) / bucketSize + 1; int[][] buckets = new int[bucketCount][2]; for (var bucket : buckets) { bucket[0] = inf; bucket[1] = -inf; } for (int v : nums) { int i = (v - mi) / bucketSize; buckets[i][0] = Math.min(buckets[i][0], v); buckets[i][1] = Math.max(buckets[i][1], v); } int prev = inf; int ans = 0; for (var bucket : buckets) { if (bucket[0] > bucket[1]) { continue; } ans = Math.max(ans, bucket[0] - prev); prev = bucket[1]; } return ans; } }
给你两个版本号 version1 和 version2 ,请你比较它们。
版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。
比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。
返回规则如下:
version1 > version2 返回 1,version1 < version2 返回 -1,0。示例 1:
输入:version1 = "1.01", version2 = "1.001" 输出:0 解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
示例 2:
输入:version1 = "1.0", version2 = "1.0.0" 输出:0 解释:version1 没有指定下标为 2 的修订号,即视为 "0"
示例 3:
输入:version1 = "0.1", version2 = "1.1" 输出:-1 解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2
提示:
1 <= version1.length, version2.length <= 500version1 和 version2 仅包含数字和 '.'version1 和 version2 都是 有效版本号version1 和 version2 的所有修订号都可以存储在 32 位整数 中方法一:双指针
同时遍历两个字符串,用两个指针 和 分别指向两个字符串的当前位置,初始时 。
每次取出两个字符串中对应的修订号,记为 和 ,比较 和 的大小,如果 ,则返回 ;如果 ,则返回 ;如果 ,则继续比较下一对修订号。
时间复杂度 ,空间复杂度 。其中 和 分别是两个字符串的长度。
class Solution { public int compareVersion(String version1, String version2) { int m = version1.length(), n = version2.length(); for (int i = 0, j = 0; i < m || j < n; ++i, ++j) { int a = 0, b = 0; while (i < m && version1.charAt(i) != '.') { a = a * 10 + (version1.charAt(i++) - '0'); } while (j < n && version2.charAt(j) != '.') { b = b * 10 + (version2.charAt(j++) - '0'); } if (a != b) { return a < b ? -1 : 1; } } return 0; } }
给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数 。
如果小数部分为循环小数,则将循环的部分括在括号内。
如果存在多个答案,只需返回 任意一个 。
对于所有给定的输入,保证 答案字符串的长度小于 104 。
示例 1:
输入:numerator = 1, denominator = 2 输出:"0.5"
示例 2:
输入:numerator = 2, denominator = 1 输出:"2"
示例 3:
输入:numerator = 4, denominator = 333 输出:"0.(012)"
提示:
-231 <= numerator, denominator <= 231 - 1denominator != 0在进行除法时使用 HashMap 存储余数及其关联的索引,这样每当出现相同的余数时,我们就知道有一个重复的小数部分。
class Solution { public String fractionToDecimal(int numerator, int denominator) { if (numerator == 0) { return "0"; } StringBuilder sb = new StringBuilder(); boolean neg = (numerator > 0) ^ (denominator > 0); sb.append(neg ? "-" : ""); long num = Math.abs((long) numerator); long d = Math.abs((long) denominator); sb.append(num / d); num %= d; if (num == 0) { return sb.toString(); } sb.append("."); Map<Long, Integer> mp = new HashMap<>(); while (num != 0) { mp.put(num, sb.length()); num *= 10; sb.append(num / d); num %= d; if (mp.containsKey(num)) { int idx = mp.get(num); sb.insert(idx, "("); sb.append(")"); break; } } return sb.toString(); } }
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
示例 1:
输入:numbers = [2,7,11,15], target = 9 输出:[1,2] 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6 输出:[1,3] 解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1 输出:[1,2] 解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
提示:
2 <= numbers.length <= 3 * 104-1000 <= numbers[i] <= 1000numbers 按 非递减顺序 排列-1000 <= target <= 1000方法一:二分查找
注意到数组按照非递减顺序排列,因此对于每个 numbers[i],可以通过二分查找的方式找到 target - numbers[i] 的位置,如果存在,那么返回 即可。
时间复杂度 ,其中 为数组 numbers 的长度。空间复杂度 。
方法二:双指针
我们定义两个指针 和 ,分别指向数组的第一个元素和最后一个元素。每次计算 ,如果和等于目标值,那么返回 即可。如果和小于目标值,那么将 右移一位,如果和大于目标值,那么将 左移一位。
时间复杂度 ,其中 为数组 numbers 的长度。空间复杂度 。
class Solution { public int[] twoSum(int[] numbers, int target) { for (int i = 0, n = numbers.length;; ++i) { int x = target - numbers[i]; int l = i + 1, r = n - 1; while (l < r) { int mid = (l + r) >> 1; if (numbers[mid] >= x) { r = mid; } else { l = mid + 1; } } if (numbers[l] == x) { return new int[] {i + 1, l + 1}; } } } }
class Solution { public int[] twoSum(int[] numbers, int target) { for (int i = 0, j = numbers.length - 1;;) { int x = numbers[i] + numbers[j]; if (x == target) { return new int[] {i + 1, j + 1}; } if (x < target) { ++i; } else { --j; } } } }
给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。
例如:
A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ...
示例 1:
输入:columnNumber = 1 输出:"A"
示例 2:
输入:columnNumber = 28 输出:"AB"
示例 3:
输入:columnNumber = 701 输出:"ZY"
示例 4:
输入:columnNumber = 2147483647 输出:"FXSHRXW"
提示:
1 <= columnNumber <= 231 - 1class Solution { public String convertToTitle(int columnNumber) { StringBuilder res = new StringBuilder(); while (columnNumber != 0) { --columnNumber; res.append((char) ('A' + columnNumber % 26)); columnNumber /= 26; } return res.reverse().toString(); } }
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3] 输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2] 输出:2
提示:
n == nums.length1 <= n <= 5 * 104-109 <= nums[i] <= 109进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
方法一:摩尔投票法
摩尔投票法的基本步骤如下:
初始化元素 ,并初始化计数器 。接下来,对于输入列表中每一个元素 :
一般而言,摩尔投票法需要对输入的列表进行两次遍历。在第一次遍历中,我们生成候选值 ,如果存在多数,那么该候选值就是多数值。在第二次遍历中,只需要简单地计算候选值的频率,以确认是否是多数值。由于本题已经明确说明存在多数值,所以第一次遍历结束后,直接返回 即可,无需二次遍历确认是否是多数值。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
class Solution { public int majorityElement(int[] nums) { int cnt = 0, m = 0; for (int x : nums) { if (cnt == 0) { m = x; cnt = 1; } else { cnt += m == x ? 1 : -1; } } return m; } }
设计一个接收整数流的数据结构,该数据结构支持检查是否存在两数之和等于特定值。
实现 TwoSum 类:
TwoSum() 使用空数组初始化 TwoSum 对象void add(int number) 向数据结构添加一个数 numberboolean find(int value) 寻找数据结构中是否存在一对整数,使得两数之和与给定的值相等。如果存在,返回 true ;否则,返回 false 。示例:
输入: ["TwoSum", "add", "add", "add", "find", "find"] [[], [1], [3], [5], [4], [7]] 输出: [null, null, null, null, true, false] 解释: TwoSum twoSum = new TwoSum(); twoSum.add(1); // [] --> [1] twoSum.add(3); // [1] --> [1,3] twoSum.add(5); // [1,3] --> [1,3,5] twoSum.find(4); // 1 + 3 = 4,返回 true twoSum.find(7); // 没有两个整数加起来等于 7 ,返回 false
提示:
-105 <= number <= 105-231 <= value <= 231 - 1104 次 add 和 find方法一:哈希表
我们用哈希表 cnt 存储数字出现的次数。
调用 add 方法时,将数字 number 的出现次数加一。
调用 find 方法时,遍历哈希表 cnt,对于每个键 x,判断 value - x 是否也是哈希表 cnt 的键,如果是,判断 x 是否等于 value - x,如果不等,说明找到了一对和为 value 的数字,返回 true;如果等,判断 x 的出现次数是否大于 1,如果大于 1,说明找到了一对和为 value 的数字,返回 true;如果小于等于 1,说明没有找到一对和为 value 的数字,继续遍历哈希表 cnt,如果遍历结束都没有找到,返回 false。
时间复杂度:
add 方法的时间复杂度为 ;find 方法的时间复杂度为 。空间复杂度 ,其中 为哈希表 cnt 的大小。
class TwoSum { private Map<Integer, Integer> cnt = new HashMap<>(); public TwoSum() { } public void add(int number) { cnt.merge(number, 1, Integer::sum); } public boolean find(int value) { for (var e : cnt.entrySet()) { int x = e.getKey(), v = e.getValue(); int y = value - x; if (cnt.containsKey(y)) { if (x != y || v > 1) { return true; } } } return false; } } /** * Your TwoSum object will be instantiated and called as such: * TwoSum obj = new TwoSum(); * obj.add(number); * boolean param_2 = obj.find(value); */
给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。
例如:
A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ...
示例 1:
输入: columnTitle = "A" 输出: 1
示例 2:
输入: columnTitle = "AB" 输出: 28
示例 3:
输入: columnTitle = "ZY" 输出: 701
提示:
1 <= columnTitle.length <= 7columnTitle 仅由大写英文组成columnTitle 在范围 ["A", "FXSHRXW"] 内class Solution { public int titleToNumber(String columnTitle) { int res = 0; for (char c : columnTitle.toCharArray()) { res = res * 26 + (c - 'A' + 1); } return res; } }
给定一个整数 n ,返回 n! 结果中尾随零的数量。
提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1
示例 1:
输入:n = 3 输出:0 解释:3! = 6 ,不含尾随 0
示例 2:
输入:n = 5 输出:1 解释:5! = 120 ,有一个尾随 0
示例 3:
输入:n = 0 输出:0
提示:
0 <= n <= 104进阶:你可以设计并实现对数时间复杂度的算法来解决此问题吗?
方法一:数学
题目实际上是求 中有多少个 的因数。
我们以 为例来分析:
class Solution { public int trailingZeroes(int n) { int ans = 0; while (n > 0) { n /= 5; ans += n; } return ans; } }
实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。int next()将指针向右移动,然后返回指针处的数字。注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。
你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。
示例:
输入 ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"] [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []] 输出 [null, 3, 7, true, 9, true, 15, true, 20, false]解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // 返回 3
bSTIterator.next(); // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 20
bSTIterator.hasNext(); // 返回 False
提示:
[1, 105] 内0 <= Node.val <= 106105 次 hasNext 和 next 操作进阶:
next() 和 hasNext() 操作均摊时间复杂度为 O(1) ,并使用 O(h) 内存。其中 h 是树的高度。方法一:递归
初始化数据时,递归中序遍历,将二叉搜索树每个结点的值保存在列表 vals 中。用 cur 指针记录外部即将遍历的位置,初始化为 0。
调用 next() 时,返回 vals[cur],同时 cur 指针自增。调用 hasNext() 时,判断 cur 指针是否已经达到 len(vals) 个数,若是,说明已经遍历结束,返回 false,否则返回 true。
方法二:栈迭代
初始化时,从根节点一路遍历所有左子节点,压入栈 stack 中。
调用 next()时,弹出栈顶元素 cur,获取 cur 的右子节点 node,若 node 不为空,一直循环压入左节点。最后返回 cur.val 即可。调用 hasNext() 时,判断 stack 是否为空,空则表示迭代结束。
class BSTIterator { private int cur = 0; private List<Integer> vals = new ArrayList<>(); public BSTIterator(TreeNode root) { inorder(root); } public int next() { return vals.get(cur++); } public boolean hasNext() { return cur < vals.size(); } private void inorder(TreeNode root) { if (root != null) { inorder(root.left); vals.add(root.val); inorder(root.right); } } } /** * Your BSTIterator object will be instantiated and called as such: * BSTIterator obj = new BSTIterator(root); * int param_1 = obj.next(); * boolean param_2 = obj.hasNext(); */
class BSTIterator { private Deque<TreeNode> stack = new LinkedList<>(); public BSTIterator(TreeNode root) { for (; root != null; root = root.left) { stack.offerLast(root); } } public int next() { TreeNode cur = stack.pollLast(); for (TreeNode node = cur.right; node != null; node = node.left) { stack.offerLast(node); } return cur.val; } public boolean hasNext() { return !stack.isEmpty(); } } /** * Your BSTIterator object will be instantiated and called as such: * BSTIterator obj = new BSTIterator(root); * int param_1 = obj.next(); * boolean param_2 = obj.hasNext(); */
恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
示例 1:
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]] 输出:7 解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。
示例 2:
输入:dungeon = [[0]] 输出:1
提示:
m == dungeon.lengthn == dungeon[i].length1 <= m, n <= 200-1000 <= dungeon[i][j] <= 1000方法一:动态规划
定义 表示从 到终点所需的最小初始值,那么 的值可以由 和 得到,即:
初始时 和 都为 ,其他位置的值为最大值。
时间复杂度 ,空间复杂度 。其中 和 分别为地牢的行数和列数。
class Solution { public int calculateMinimumHP(int[][] dungeon) { int m = dungeon.length, n = dungeon[0].length; int[][] dp = new int[m + 1][n + 1]; for (var e : dp) { Arrays.fill(e, 1 << 30); } dp[m][n - 1] = dp[m - 1][n] = 1; for (int i = m - 1; i >= 0; --i) { for (int j = n - 1; j >= 0; --j) { dp[i][j] = Math.max(1, Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]); } } return dp[0][0]; } }
表: Person
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | PersonId | int | | FirstName | varchar | | LastName | varchar | +-------------+---------+ personId 是该表的主键列。 该表包含一些人的 ID 和他们的姓和名的信息。
表: Address
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | AddressId | int | | PersonId | int | | City | varchar | | State | varchar | +-------------+---------+ addressId 是该表的主键列。 该表的每一行都包含一个 ID = PersonId 的人的城市和州的信息。
编写一个SQL查询来报告 Person 表中每个人的姓、名、城市和州。如果 personId 的地址不在 Address 表中,则报告为空 null 。
以 任意顺序 返回结果表。
查询结果格式如下所示。
示例 1:
输入: Person表: +----------+----------+-----------+ | personId | lastName | firstName | +----------+----------+-----------+ | 1 | Wang | Allen | | 2 | Alice | Bob | +----------+----------+-----------+ Address表: +-----------+----------+---------------+------------+ | addressId | personId | city | state | +-----------+----------+---------------+------------+ | 1 | 2 | New York City | New York | | 2 | 3 | Leetcode | California | +-----------+----------+---------------+------------+ 输出: +-----------+----------+---------------+----------+ | firstName | lastName | city | state | +-----------+----------+---------------+----------+ | Allen | Wang | Null | Null | | Bob | Alice | New York City | New York | +-----------+----------+---------------+----------+ 解释: 地址表中没有 personId = 1 的地址,所以它们的城市和州返回 null。 addressId = 1 包含了 personId = 2 的地址信息。
左连接。
SELECT p.FirstName, p.LastName, a.City, a.State FROM Person p LEFT JOIN Address a ON p.PersonId = a.PersonId;
Employee 表:
+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | salary | int | +-------------+------+ id 是这个表的主键。 表的每一行包含员工的工资信息。
编写一个 SQL 查询,获取并返回 Employee 表中第二高的薪水 。如果不存在第二高的薪水,查询应该返回 null 。
查询结果如下例所示。
示例 1:
输入: Employee 表: +----+--------+ | id | salary | +----+--------+ | 1 | 100 | | 2 | 200 | | 3 | 300 | +----+--------+ 输出: +---------------------+ | SecondHighestSalary | +---------------------+ | 200 | +---------------------+
示例 2:
输入: Employee 表: +----+--------+ | id | salary | +----+--------+ | 1 | 100 | +----+--------+ 输出: +---------------------+ | SecondHighestSalary | +---------------------+ | null | +---------------------+
解法 1:使用 LIMIT 语句和子查询。
# Write your MySQL query statement below SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1 OFFSET 1 ) AS SecondHighestSalary;
解法 2:使用 MAX() 函数,从小于 MAX() 的 Salary 中挑选最大值 MAX() 即可。
# Write your MySQL query statement below SELECT MAX(Salary) AS SecondHighestSalary FROM Employee WHERE Salary < ( SELECT MAX(Salary) FROM Employee );
表: Employee
+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | salary | int | +-------------+------+ Id是该表的主键列。 该表的每一行都包含有关员工工资的信息。
编写一个SQL查询来报告 Employee 表中第 n 高的工资。如果没有第 n 个最高工资,查询应该报告为 null 。
查询结果格式如下所示。
示例 1:
输入: Employee table: +----+--------+ | id | salary | +----+--------+ | 1 | 100 | | 2 | 200 | | 3 | 300 | +----+--------+ n = 2 输出: +------------------------+ | getNthHighestSalary(2) | +------------------------+ | 200 | +------------------------+
示例 2:
输入: Employee 表: +----+--------+ | id | salary | +----+--------+ | 1 | 100 | +----+--------+ n = 2 输出: +------------------------+ | getNthHighestSalary(2) | +------------------------+ | null | +------------------------+
CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT BEGIN SET N = N - 1; RETURN ( # Write your MySQL query statement below. SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1 OFFSET N ) ); END
表: Scores
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | score | decimal | +-------------+---------+ Id是该表的主键。 该表的每一行都包含了一场比赛的分数。Score是一个有两位小数点的浮点值。
编写 SQL 查询对分数进行排序。排名按以下规则计算:
按 score 降序返回结果表。
查询结果格式如下所示。
示例 1:
输入: Scores 表: +----+-------+ | id | score | +----+-------+ | 1 | 3.50 | | 2 | 3.65 | | 3 | 4.00 | | 4 | 3.85 | | 5 | 4.00 | | 6 | 3.65 | +----+-------+ 输出: +-------+------+ | score | rank | +-------+------+ | 4.00 | 1 | | 4.00 | 1 | | 3.85 | 2 | | 3.65 | 3 | | 3.65 | 3 | | 3.50 | 4 | +-------+------+
使用 DENSE_RANK() 函数,语法如下:
DENSE_RANK() OVER ( PARTITION BY <expression>[{,<expression>...}] ORDER BY <expression> [ASC|DESC], [{,<expression>...}] )
在这个语法中:
PARTITION BY 子句将 FROM 子句生成的结果集划分为分区。DENSE_RANK()函数应用于每个分区。ORDER BY 子句指定 DENSE_RANK() 函数操作的每个分区中的行顺序。与 RANK() 函数不同,DENSE_RANK() 函数始终返回连续的排名值。
题解如下:
# Write your MySQL query statement below SELECT Score, DENSE_RANK() OVER (ORDER BY Score DESC) 'Rank' FROM Scores;
MySQL 8 开始才提供了 ROW_NUMBER(),RANK(),DENSE_RANK() 等窗口函数,在之前的版本,可以使用变量实现类似的功能:
SELECT Score, CONVERT(rk, SIGNED) `Rank` FROM (SELECT Score, IF(@latest = Score, @rank, @rank := @rank + 1) rk, @latest := Score FROM Scores, (SELECT @rank := 0, @latest := NULL) tmp ORDER BY Score DESC) s;
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:"210"
示例 2:
输入:nums = [3,30,34,5,9]
输出:"9534330"
提示:
1 <= nums.length <= 1000 <= nums[i] <= 109方法一:自定义排序
先转成字符串列表,再对字符串列表进行字典序降序排列。最后将列表所有字符串拼接即可。
class Solution { public String largestNumber(int[] nums) { List<String> vs = new ArrayList<>(); for (int v : nums) { vs.add(v + ""); } vs.sort((a, b) -> (b + a).compareTo(a + b)); if ("0".equals(vs.get(0))) { return "0"; } return String.join("", vs); } }
表:Logs
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | num | varchar | +-------------+---------+ id 是这个表的主键。
编写一个 SQL 查询,查找所有至少连续出现三次的数字。
返回的结果表中的数据可以按 任意顺序 排列。
查询结果格式如下面的例子所示:
示例 1:
输入: Logs 表: +----+-----+ | Id | Num | +----+-----+ | 1 | 1 | | 2 | 1 | | 3 | 1 | | 4 | 2 | | 5 | 1 | | 6 | 2 | | 7 | 2 | +----+-----+ 输出: Result 表: +-----------------+ | ConsecutiveNums | +-----------------+ | 1 | +-----------------+ 解释:1 是唯一连续出现至少三次的数字。
select distinct(Num) as ConsecutiveNums from Logs Curr where Num = (select Num from Logs where id = Curr.id - 1) and Num = (select Num from Logs where id = Curr.id - 2)
# Write your MySQL query statement below SELECT DISTINCT l1.num AS ConsecutiveNums FROM logs AS l1, logs AS l2, logs AS l3 WHERE l1.id = l2.id - 1 AND l2.id = l3.id - 1 AND l1.num = l2.num AND l2.num = l3.num
表:Employee
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | name | varchar | | salary | int | | managerId | int | +-------------+---------+ Id是该表的主键。 该表的每一行都表示雇员的ID、姓名、工资和经理的ID。
编写一个SQL查询来查找收入比经理高的员工。
以 任意顺序 返回结果表。
查询结果格式如下所示。
示例 1:
输入: Employee 表: +----+-------+--------+-----------+ | id | name | salary | managerId | +----+-------+--------+-----------+ | 1 | Joe | 70000 | 3 | | 2 | Henry | 80000 | 4 | | 3 | Sam | 60000 | Null | | 4 | Max | 90000 | Null | +----+-------+--------+-----------+ 输出: +----------+ | Employee | +----------+ | Joe | +----------+ 解释: Joe 是唯一挣得比经理多的雇员。
select Name as Employee from Employee Curr where Salary > ( select Salary from Employee where Id = Curr.ManagerId )
表: Person
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | email | varchar | +-------------+---------+ id 是该表的主键列。 此表的每一行都包含一封电子邮件。电子邮件不包含大写字母。
编写一个 SQL 查询来报告所有重复的电子邮件。 请注意,可以保证电子邮件字段不为 NULL。
以 任意顺序 返回结果表。
查询结果格式如下例。
示例 1:
输入: Person 表: +----+---------+ | id | email | +----+---------+ | 1 | a@b.com | | 2 | c@d.com | | 3 | a@b.com | +----+---------+ 输出: +---------+ | Email | +---------+ | a@b.com | +---------+ 解释: a@b.com 出现了两次。
SELECT Email FROM Person GROUP BY Email HAVING count(Email) > 1;
SELECT DISTINCT p1.email FROM person AS p1, person AS p2 WHERE p1.id != p2.id AND p1.email = p2.email;
某网站包含两个表,Customers 表和 Orders 表。编写一个 SQL 查询,找出所有从不订购任何东西的客户。
Customers 表:
+----+-------+ | Id | Name | +----+-------+ | 1 | Joe | | 2 | Henry | | 3 | Sam | | 4 | Max | +----+-------+
Orders 表:
+----+------------+ | Id | CustomerId | +----+------------+ | 1 | 3 | | 2 | 1 | +----+------------+
例如给定上述表格,你的查询应返回:
+-----------+ | Customers | +-----------+ | Henry | | Max | +-----------+
方法一:
列举所有已存在订单的客户 ID,使用 NOT IN 找到不存在其中的客户。
方法二:
使用 LEFT JOIN 连接表格,返回 CustomerId 为 NULL 的数据。
select Name as Customers from Customers where id not in ( select CustomerId from Orders );
SELECT c.Name AS Customers FROM customers AS c LEFT JOIN orders AS o ON c.Id = o.CustomerId WHERE o.CustomerId IS NULL;
表: Employee
+--------------+---------+ | 列名 | 类型 | +--------------+---------+ | id | int | | name | varchar | | salary | int | | departmentId | int | +--------------+---------+ id是此表的主键列。 departmentId是Department表中ID的外键。 此表的每一行都表示员工的ID、姓名和工资。它还包含他们所在部门的ID。
表: Department
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+ id是此表的主键列。 此表的每一行都表示一个部门的ID及其名称。
编写SQL查询以查找每个部门中薪资最高的员工。
按 任意顺序 返回结果表。
查询结果格式如下例所示。
示例 1:
输入: Employee 表: +----+-------+--------+--------------+ | id | name | salary | departmentId | +----+-------+--------+--------------+ | 1 | Joe | 70000 | 1 | | 2 | Jim | 90000 | 1 | | 3 | Henry | 80000 | 2 | | 4 | Sam | 60000 | 2 | | 5 | Max | 90000 | 1 | +----+-------+--------+--------------+ Department 表: +----+-------+ | id | name | +----+-------+ | 1 | IT | | 2 | Sales | +----+-------+ 输出: +------------+----------+--------+ | Department | Employee | Salary | +------------+----------+--------+ | IT | Jim | 90000 | | Sales | Henry | 80000 | | IT | Max | 90000 | +------------+----------+--------+ 解释:Max 和 Jim 在 IT 部门的工资都是最高的,Henry 在销售部的工资最高。
SELECT Department.NAME AS Department, Employee.NAME AS Employee, Salary FROM Employee, Department WHERE Employee.DepartmentId = Department.Id AND ( Employee.DepartmentId, Salary ) IN (SELECT DepartmentId, max( Salary ) FROM Employee GROUP BY DepartmentId )
# Write your MySQL query statement below SELECT d.NAME AS Department, e1.NAME AS Employee, e1.salary AS Salary FROM Employee AS e1 JOIN Department AS d ON e1.departmentId = d.id WHERE e1.salary = ( SELECT MAX( Salary ) FROM Employee AS e2 WHERE e2.departmentId = d.id )
表: Employee
+--------------+---------+ | Column Name | Type | +--------------+---------+ | id | int | | name | varchar | | salary | int | | departmentId | int | +--------------+---------+ Id是该表的主键列。 departmentId是Department表中ID的外键。 该表的每一行都表示员工的ID、姓名和工资。它还包含了他们部门的ID。
表: Department
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+ Id是该表的主键列。 该表的每一行表示部门ID和部门名。
公司的主管们感兴趣的是公司每个部门中谁赚的钱最多。一个部门的 高收入者 是指一个员工的工资在该部门的 不同 工资中 排名前三 。
编写一个SQL查询,找出每个部门中 收入高的员工 。
以 任意顺序 返回结果表。
查询结果格式如下所示。
示例 1:
输入: Employee 表: +----+-------+--------+--------------+ | id | name | salary | departmentId | +----+-------+--------+--------------+ | 1 | Joe | 85000 | 1 | | 2 | Henry | 80000 | 2 | | 3 | Sam | 60000 | 2 | | 4 | Max | 90000 | 1 | | 5 | Janet | 69000 | 1 | | 6 | Randy | 85000 | 1 | | 7 | Will | 70000 | 1 | +----+-------+--------+--------------+ Department 表: +----+-------+ | id | name | +----+-------+ | 1 | IT | | 2 | Sales | +----+-------+ 输出: +------------+----------+--------+ | Department | Employee | Salary | +------------+----------+--------+ | IT | Max | 90000 | | IT | Joe | 85000 | | IT | Randy | 85000 | | IT | Will | 70000 | | Sales | Henry | 80000 | | Sales | Sam | 60000 | +------------+----------+--------+ 解释: 在IT部门: - Max的工资最高 - 兰迪和乔都赚取第二高的独特的薪水 - 威尔的薪水是第三高的 在销售部: - 亨利的工资最高 - 山姆的薪水第二高 - 没有第三高的工资,因为只有两名员工
SELECT Department.NAME AS Department, Employee.NAME AS Employee, Salary FROM Employee, Department WHERE Employee.DepartmentId = Department.Id AND (SELECT COUNT(DISTINCT e2.Salary) FROM Employee e2 WHERE e2.Salary > Employee.Salary AND Employee.DepartmentId = e2.DepartmentId ) < 3
给你一个字符数组 s ,反转其中 单词 的顺序。
单词 的定义为:单词是一个由非空格字符组成的序列。s 中的单词将会由单个空格分隔。
必须设计并实现 原地 解法来解决此问题,即不分配额外的空间。
示例 1:
输入:s = ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] 输出:["b","l","u","e"," ","i","s"," ","s","k","y"," ","t","h","e"]
示例 2:
输入:s = ["a"] 输出:["a"]
提示:
1 <= s.length <= 105s[i] 可以是一个英文字母(大写或小写)、数字、或是空格 ' ' 。s 中至少存在一个单词s 不含前导或尾随空格s 中的每个单词都由单个空格分隔先翻转里面每个单词,最后再将字符串整体翻转。
class Solution { public void reverseWords(char[] s) { int n = s.length; for (int i = 0, j = 0; j < n; ++j) { if (s[j] == ' ') { reverse(s, i, j - 1); i = j + 1; } else if (j == n - 1) { reverse(s, i, j); } } reverse(s, 0, n - 1); } private void reverse(char[] s, int i, int j) { for (; i < j; ++i, --j) { char t = s[i]; s[i] = s[j]; s[j] = t; } } }
DNA序列 由一系列核苷酸组成,缩写为 'A', 'C', 'G' 和 'T'.。
"ACGAATTCCG" 是一个 DNA序列 。在研究 DNA 时,识别 DNA 中的重复序列非常有用。
给定一个表示 DNA序列 的字符串 s ,返回所有在 DNA 分子中出现不止一次的 长度为 10 的序列(子字符串)。你可以按 任意顺序 返回答案。
示例 1:
输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT" 输出:["AAAAACCCCC","CCCCCAAAAA"]
示例 2:
输入:s = "AAAAAAAAAAAAA" 输出:["AAAAAAAAAA"]
提示:
0 <= s.length <= 105s[i]=='A'、'C'、'G' or 'T'方法一:哈希表
朴素解法,用哈希表保存所有长度为 10 的子序列出现的次数,当子序列出现次数大于 1 时,把该子序列作为结果之一。
假设字符串 s 长度为 n,则时间复杂度 ,空间复杂度 。
方法二:Rabin-Karp 字符串匹配算法
本质上是滑动窗口和哈希的结合方法,和 0028.找出字符串中第一个匹配项的下标 类似,本题可以借助哈希函数将子序列计数的时间复杂度降低到 。
假设字符串 s 长度为 n,则时间复杂度为 ,空间复杂度 。
class Solution { public List<String> findRepeatedDnaSequences(String s) { int n = s.length() - 10; Map<String, Integer> cnt = new HashMap<>(); List<String> ans = new ArrayList<>(); for (int i = 0; i <= n; ++i) { String sub = s.substring(i, i + 10); cnt.put(sub, cnt.getOrDefault(sub, 0) + 1); if (cnt.get(sub) == 2) { ans.add(sub); } } return ans; } }
哈希表:
Rabin-Karp:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格,和一个整型 k 。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1] 输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
0 <= k <= 1000 <= prices.length <= 10000 <= prices[i] <= 1000方法一:记忆化搜索
我们设计一个函数 ,表示从第 天开始,最多进行 笔交易,以及当前持有股票的状态为 (不持有股票用 表示,持有股票用 表示)时,所能获得的最大利润。答案即为 。
函数 的执行逻辑如下:
取上述三种情况的最大值即为 的值。
过程中,我们可以使用记忆化搜索的方法,将每次计算的结果保存下来,避免重复计算。
时间复杂度 ,空间复杂度 。其中 和 分别为数组 的长度和 的值。
class Solution { private Integer[][][] f; private int[] prices; private int n; public int maxProfit(int k, int[] prices) { n = prices.length; this.prices = prices; f = new Integer[n][k + 1][2]; return dfs(0, k, 0); } private int dfs(int i, int j, int k) { if (i >= n) { return 0; } if (f[i][j][k] != null) { return f[i][j][k]; } int ans = dfs(i + 1, j, k); if (k > 0) { ans = Math.max(ans, prices[i] + dfs(i + 1, j, 0)); } else if (j > 0) { ans = Math.max(ans, -prices[i] + dfs(i + 1, j - 1, 1)); } f[i][j][k] = ans; return ans; } }
class Solution { public int maxProfit(int k, int[] prices) { int n = prices.length; if (n <= 1) { return 0; } int[][][] dp = new int[n][k + 1][2]; for (int i = 1; i <= k; ++i) { dp[0][i][1] = -prices[0]; } for (int i = 1; i < n; ++i) { for (int j = 1; j <= k; ++j) { dp[i][j][0] = Math.max(dp[i - 1][j][1] + prices[i], dp[i - 1][j][0]); dp[i][j][1] = Math.max(dp[i - 1][j - 1][0] - prices[i], dp[i - 1][j][1]); } } return dp[n - 1][k][0]; } }
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 105-231 <= nums[i] <= 231 - 10 <= k <= 105进阶:
O(1) 的 原地 算法解决这个问题吗?方法一:三次翻转
我们不妨记数组长度为 ,然后将 对 取模,得到实际需要旋转的步数 。
接下来,我们进行三次翻转,即可得到最终结果:
举个例子,对于数组 , , , 。
时间复杂度 ,其中 为数组长度。空间复杂度 。
class Solution { private int[] nums; public void rotate(int[] nums, int k) { this.nums = nums; int n = nums.length; k %= n; reverse(0, n - 1); reverse(0, k - 1); reverse(k, n - 1); } private void reverse(int i, int j) { for (; i < j; ++i, --j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } } }
颠倒给定的 32 位无符号整数的二进制位。
提示:
-3,输出表示有符号整数 -1073741825。示例 1:
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:
输入:n = 11111111111111111111111111111101 输出:3221225471 (10111111111111111111111111111111) 解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293, 因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
提示:
32 的二进制字符串进阶: 如果多次调用这个函数,你将如何优化你的算法?
public class Solution { // you need treat n as an unsigned value public int reverseBits(int n) { int res = 0; for (int i = 0; i < 32 && n != 0; ++i) { res |= ((n & 1) << (31 - i)); n >>>= 1; } return res; } }
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。
提示:
-3。示例 1:
输入:n = 00000000000000000000000000001011 输出:3 解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:n = 00000000000000000000000010000000 输出:1 解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:n = 11111111111111111111111111111101 输出:31 解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
提示:
32 的 二进制串 。进阶:
方法一:位运算
利用 n & (n - 1) 消除 n 中最后一位 1 这一特点,优化过程:
HAMMING-WEIGHT(n)
r = 0
while n != 0
n &= n - 1
r += 1
r
以 5 为例,演示推演过程:
[0, 1, 0, 1] // 5 [0, 1, 0, 0] // 5 - 1 = 4 [0, 1, 0, 0] // 5 & 4 = 4 [0, 1, 0, 0] // 4 [0, 0, 1, 1] // 4 - 1 = 3 [0, 0, 0, 0] // 4 & 3 = 0
方法二:lowbit
x -= (x & -x) 可以消除二进制形式的最后一位 1。
public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { int ans = 0; while (n != 0) { n &= n - 1; ++ans; } return ans; } }
public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { int ans = 0; while (n != 0) { n -= (n & -n); ++ans; } return ans; } }
写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。
为了简单起见,你可以假设:
words.txt只包括小写字母和 ' ' 。示例:
假设 words.txt 内容如下:
the day is sunny the the the sunny is is
你的脚本应当输出(以词频降序排列):
the 4 is 3 sunny 2 day 1
说明:
给定一个包含电话号码列表(一行一个电话号码)的文本文件 file.txt,写一个单行 bash 脚本输出所有有效的电话号码。
你可以假设一个有效的电话号码必须满足以下两种格式: (xxx) xxx-xxxx 或 xxx-xxx-xxxx。(x 表示一个数字)
你也可以假设每行前后没有多余的空格字符。
示例:
假设 file.txt 内容如下:
987-123-4567 123 456 7890 (123) 456-7890
你的脚本应当输出下列有效的电话号码:
987-123-4567 (123) 456-7890
给定一个文件 file.txt,转置它的内容。
你可以假设每行列数相同,并且每个字段由 ' ' 分隔。
示例:
假设 file.txt 文件内容如下:
name age alice 21 ryan 30
应当输出:
name alice ryan age 21 30
给定一个文本文件 file.txt,请只打印这个文件中的第十行。
示例:
假设 file.txt 有如下内容:
Line 1 Line 2 Line 3 Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 Line 10
你的脚本应当显示第十行:
Line 10
说明:
1. 如果文件少于十行,你应当输出什么?
2. 至少有三种不同的解法,请尝试尽可能多的方法来解题。
表: Person
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | email | varchar | +-------------+---------+ id是该表的主键列。 该表的每一行包含一封电子邮件。电子邮件将不包含大写字母。
编写一个 SQL 删除语句来 删除 所有重复的电子邮件,只保留一个id最小的唯一电子邮件。
以 任意顺序 返回结果表。 (注意: 仅需要写删除语句,将自动对剩余结果进行查询)
查询结果格式如下所示。
示例 1:
输入: Person 表: +----+------------------+ | id | email | +----+------------------+ | 1 | john@example.com | | 2 | bob@example.com | | 3 | john@example.com | +----+------------------+ 输出: +----+------------------+ | id | email | +----+------------------+ | 1 | john@example.com | | 2 | bob@example.com | +----+------------------+ 解释: john@example.com重复两次。我们保留最小的Id = 1。
DELETE FROM Person WHERE Id NOT IN ( SELECT MIN( Id ) FROM ( SELECT * FROM Person ) AS p GROUP BY p.Email );
DELETE p2 FROM person AS p1 JOIN person AS p2 ON p1.email = p2.email WHERE p1.id < p2.id;
表: Weather
+---------------+---------+ | Column Name | Type | +---------------+---------+ | id | int | | recordDate | date | | temperature | int | +---------------+---------+ id 是这个表的主键 该表包含特定日期的温度信息
编写一个 SQL 查询,来查找与之前(昨天的)日期相比温度更高的所有日期的 id 。
返回结果 不要求顺序 。
查询结果格式如下例。
示例 1:
输入: Weather 表: +----+------------+-------------+ | id | recordDate | Temperature | +----+------------+-------------+ | 1 | 2015-01-01 | 10 | | 2 | 2015-01-02 | 25 | | 3 | 2015-01-03 | 20 | | 4 | 2015-01-04 | 30 | +----+------------+-------------+ 输出: +----+ | id | +----+ | 2 | | 4 | +----+ 解释: 2015-01-02 的温度比前一天高(10 -> 25) 2015-01-04 的温度比前一天高(20 -> 30)
select w1.Id from Weather w1, Weather w2 where DATEDIFF(w1.RecordDate, w2.RecordDate) = 1 and w1.Temperature > w2.Temperature
SELECT w2.id AS Id FROM weather AS w1 JOIN weather AS w2 ON DATE_ADD( w1.recordDate, INTERVAL 1 DAY) = w2.recordDate WHERE w1.temperature < w2.temperature
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400方法一:记忆化搜索
我们设计函数 表示从第 间房屋开始偷窃,能偷窃到的最高金额。答案为 。
对于第 间房屋,有偷窃和不偷窃两种选择。如果偷窃,那么下一间房屋就不能偷窃,偷窃总金额为当前房屋金额加上下下间房屋开始的偷窃最高金额,即 。如果不偷窃,那么下一间房屋就可以偷窃,偷窃总金额为下一间房屋开始的偷窃最高金额,即 。两种选择取最大值作为函数 的返回值。
我们可以使用记忆化搜索,避免重复计算。
时间复杂度 ,空间复杂度 。其中 为房屋数量。
方法二:动态规划
我们也可以将记忆化搜索改成动态规划。
定义 表示偷窃前 个房屋能得到的最高金额。答案为 。
状态转移方程为 。
时间复杂度 ,空间复杂度 。其中 为房屋数量。
方法三:动态规划(空间优化)
注意到方法二中的状态转移方程只和 和 有关,因此我们可以只用两个变量来维护这两个状态,从而将空间复杂度优化到 。
时间复杂度 ,空间复杂度 。其中 为房屋数量。
class Solution { private int[] f; private int[] nums; public int rob(int[] nums) { this.nums = nums; f = new int[nums.length]; Arrays.fill(f, -1); return dfs(0); } private int dfs(int i) { if (i >= nums.length) { return 0; } if (f[i] != -1) { return f[i]; } f[i] = Math.max(nums[i] + dfs(i + 2), dfs(i + 1)); return f[i]; } }
class Solution { public int rob(int[] nums) { int n = nums.length; int[] dp = new int[n + 1]; dp[1] = nums[0]; for (int i = 2; i <= n; ++i) { dp[i] = Math.max(nums[i - 1] + dp[i - 2], dp[i - 1]); } return dp[n]; } }
class Solution { public int rob(int[] nums) { int a = 0, b = nums[0]; for (int i = 1; i < nums.length; ++i) { int c = Math.max(nums[i] + a, b); a = b; b = c; } return b; } }
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:

输入: [1,2,3,null,5,null,4] 输出: [1,3,4]
示例 2:
输入: [1,null,3] 输出: [1,3]
示例 3:
输入: [] 输出: []
提示:
[0,100]-100 <= Node.val <= 100 方法一:BFS
使用 BFS 层序遍历二叉树,每层最后一个节点即为该层的右视图节点。
时间复杂度 ,空间复杂度 。其中 为二叉树节点个数。
方法二:DFS
使用 DFS 深度优先遍历二叉树,每次先遍历右子树,再遍历左子树,这样每层第一个遍历到的节点即为该层的右视图节点。
时间复杂度 ,空间复杂度 。其中 为二叉树节点个数。
class Solution { public List<Integer> rightSideView(TreeNode root) { List<Integer> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root); while (!q.isEmpty()) { ans.add(q.peekLast().val); for (int n = q.size(); n > 0; --n) { TreeNode node = q.poll(); if (node.left != null) { q.offer(node.left); } if (node.right != null) { q.offer(node.right); } } } return ans; } }
class Solution { private List<Integer> ans = new ArrayList<>(); public List<Integer> rightSideView(TreeNode root) { dfs(root, 0); return ans; } private void dfs(TreeNode node, int depth) { if (node == null) { return; } if (depth == ans.size()) { ans.add(node.val); } dfs(node.right, depth + 1); dfs(node.left, depth + 1); } }
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
示例 2:
输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 300grid[i][j] 的值为 '0' 或 '1'方法一:Flood fill 算法
Flood fill 算法是从一个区域中提取若干个连通的点与其他相邻区域区分开(或分别染成不同颜色)的经典算法。因为其思路类似洪水从一个区域扩散到所有能到达的区域而得名。
最简单的实现方法是采用 DFS 的递归方法,也可以采用 BFS 的迭代来实现。
时间复杂度 ,空间复杂度 。其中 和 分别为网格的行数和列数。
方法二:并查集
并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的合并及查询问题。 它支持两种操作:
其中 为阿克曼函数的反函数,其增长极其缓慢,也就是说其单次操作的平均运行时间可以认为是一个很小的常数。
以下是并查集的常用模板,需要熟练掌握。其中:
n 表示节点数p 存储每个点的父节点,初始时每个点的父节点都是自己size 只有当节点是祖宗节点时才有意义,表示祖宗节点所在集合中,点的数量find(x) 函数用于查找 所在集合的祖宗节点union(a, b) 函数用于合并 和 所在的集合时间复杂度 。其中 和 分别为网格的行数和列数。
DFS - Flood Fill 算法:
BFS - Flood Fill 算法:
并查集:
DFS - Flood Fill 算法:
class Solution { private char[][] grid; private int m; private int n; public int numIslands(char[][] grid) { m = grid.length; n = grid[0].length; this.grid = grid; int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '1') { dfs(i, j); ++ans; } } } return ans; } private void dfs(int i, int j) { grid[i][j] = '0'; int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') { dfs(x, y); } } } }
BFS - Flood Fill 算法:
class Solution { private char[][] grid; private int m; private int n; public int numIslands(char[][] grid) { m = grid.length; n = grid[0].length; this.grid = grid; int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '1') { bfs(i, j); ++ans; } } } return ans; } private void bfs(int i, int j) { grid[i][j] = '0'; Deque<int[]> q = new ArrayDeque<>(); q.offer(new int[]{i, j}); int[] dirs = {-1, 0, 1, 0, -1}; while (!q.isEmpty()) { int[] p = q.poll(); for (int k = 0; k < 4; ++k) { int x = p[0] + dirs[k]; int y = p[1] + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') { q.offer(new int[]{x, y}); grid[x][y] = '0'; } } } } }
并查集:
class Solution { private int[] p; public int numIslands(char[][] grid) { int m = grid.length; int n = grid[0].length; p = new int[m * n]; for (int i = 0; i < p.length; ++i) { p[i] = i; } int[] dirs = {1, 0, 1}; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '1') { for (int k = 0; k < 2; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x < m && y < n && grid[x][y] == '1') { p[find(x * n + y)] = find(i * n + j); } } } } } int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '1' && i * n + j == find(i * n + j)) { ++ans; } } } return ans; } private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; } }
DFS - Flood Fill 算法:
BFS - Flood Fill 算法:
并查集:
DFS - Flood Fill 算法:
BFS - Flood Fill 算法:
并查集:
DFS - Flood Fill 算法:
BFS - Flood Fill 算法:
并查集:
DFS - Flood Fill 算法:
BFS - Flood Fill 算法:
并查集:
给你两个整数 left 和 right ,表示区间 [left, right] ,返回此区间内所有数字 按位与 的结果(包含 left 、right 端点)。
示例 1:
输入:left = 5, right = 7 输出:4
示例 2:
输入:left = 0, right = 0 输出:0
示例 3:
输入:left = 1, right = 2147483647 输出:0
提示:
0 <= left <= right <= 231 - 1方法一:位运算
题目可以转换为求数字的公共二进制前缀。
当 时,我们循环将 的最后一个二进制位 变成 ,直到 ,此时 即为数字的公共二进制前缀,返回 即可。
时间复杂度 ,空间复杂度 。
class Solution { public int rangeBitwiseAnd(int left, int right) { while (left < right) { right &= (right - 1); } return right; } }
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19 输出:true 解释: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
示例 2:
输入:n = 2 输出:false
提示:
1 <= n <= 231 - 1方法一:哈希表 + 模拟
将每次转换后的数字存入哈希表,如果出现重复数字,说明进入了循环,不是快乐数。否则,如果转换后的数字为 ,说明是快乐数。
时间复杂度 ,空间复杂度 。
方法二:快慢指针
与判断链表是否存在环原理一致。如果 是快乐数,那么快指针最终会与慢指针相遇,且相遇时的数字为 ;否则,快指针最终会与慢指针相遇,且相遇时的数字不为 。
因此,最后判断快慢指针相遇时的数字是否为 即可。
时间复杂度 ,空间复杂度 。
class Solution { public boolean isHappy(int n) { Set<Integer> vis = new HashSet<>(); while (n != 1 && !vis.contains(n)) { vis.add(n); int x = 0; while (n != 0) { x += (n % 10) * (n % 10); n /= 10; } n = x; } return n == 1; } }
class Solution { public boolean isHappy(int n) { int slow = n, fast = next(n); while (slow != fast) { slow = next(slow); fast = next(next(fast)); } return slow == 1; } private int next(int x) { int y = 0; for (; x > 0; x /= 10) { y += (x % 10) * (x % 10); } return y; } }
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1 输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]
提示:
[0, 104] 内1 <= Node.val <= 500 <= val <= 50class Solution { public ListNode removeElements(ListNode head, int val) { ListNode dummy = new ListNode(-1, head); ListNode pre = dummy; while (pre.next != null) { if (pre.next.val != val) pre = pre.next; else pre.next = pre.next.next; } return dummy.next; } }
给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。
示例 1:
输入:n = 10 输出:4 解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:
输入:n = 0 输出:0
示例 3:
输入:n = 1 输出:0
提示:
0 <= n <= 5 * 106方法一:埃氏筛
如果 是质数,那么大于 的 的倍数 ,,… 一定不是质数,因此我们可以从这里入手。
设 表示数 是不是质数,如果是质数则为 ,否则为 。
我们在 范围内顺序遍历每个数 ,如果这个数为质数(),质数个数增 1,然后将其所有的倍数 都标记为合数(除了该质数本身),即 ,这样在运行结束的时候我们即能知道质数的个数。
时间复杂度 。
class Solution { public int countPrimes(int n) { boolean[] primes = new boolean[n]; Arrays.fill(primes, true); int ans = 0; for (int i = 2; i < n; ++i) { if (primes[i]) { ++ans; for (int j = i + i; j < n; j += i) { primes[j] = false; } } } return ans; } }
给定两个字符串 s 和 t ,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
示例 1:
输入:s = "egg", t = "add" 输出:true
示例 2:
输入:s = "foo", t = "bar" 输出:false
示例 3:
输入:s = "paper", t = "title" 输出:true
提示:
1 <= s.length <= 5 * 104t.length == s.lengths 和 t 由任意有效的 ASCII 字符组成方法一:哈希表或数组
我们可以用两个哈希表或数组 和 记录 和 中字符的映射关系。
遍历 和 ,如果 和 中对应的字符映射关系不同,则返回 false,否则更新 和 中对应的字符映射关系。遍历结束,说明 和 是同构的,返回 true。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度;而 为字符集大小,本题中 。
class Solution { public boolean isIsomorphic(String s, String t) { Map<Character, Character> d1 = new HashMap<>(); Map<Character, Character> d2 = new HashMap<>(); int n = s.length(); for (int i = 0; i < n; ++i) { char a = s.charAt(i), b = t.charAt(i); if (d1.containsKey(a) && d1.get(a) != b) { return false; } if (d2.containsKey(b) && d2.get(b) != a) { return false; } d1.put(a, b); d2.put(b, a); } return true; } }
class Solution { public boolean isIsomorphic(String s, String t) { int[] d1 = new int[256]; int[] d2 = new int[256]; int n = s.length(); for (int i = 0; i < n; ++i) { char a = s.charAt(i), b = t.charAt(i); if (d1[a] != d2[b]) { return false; } d1[a] = i + 1; d2[b] = i + 1; } return true; } }
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2] 输出:[2,1]
示例 3:
输入:head = [] 输出:[]
提示:
[0, 5000]-5000 <= Node.val <= 5000进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
方法一:头插法
创建虚拟头节点 ,遍历链表,将每个节点依次插入 的下一个节点。遍历结束,返回 。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
方法二:递归
递归反转链表的第二个节点到尾部的所有节点,然后 插在反转后的链表的尾部。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
迭代版本:
class Solution { public ListNode reverseList(ListNode head) { ListNode dummy = new ListNode(); ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = dummy.next; dummy.next = curr; curr = next; } return dummy.next; } }
递归版本:
class Solution { public ListNode reverseList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode ans = reverseList(head.next); head.next.next = head; head.next = null; return ans; } }
循环:
递归:
循环:
递归:
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
[0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]] 输出:true 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]] 输出:false 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 1050 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= ai, bi < numCoursesprerequisites[i] 中的所有课程对 互不相同方法一:拓扑排序
对于本题,我们可以将课程看作图中的节点,先修课程看作图中的边,那么我们可以将本题转化为判断有向图中是否存在环。
具体地,我们可以使用拓扑排序的思想,对于每个入度为 的节点,我们将其出度的节点的入度减 ,直到所有节点都被遍历到。
如果所有节点都被遍历到,说明图中不存在环,那么我们就可以完成所有课程的学习;否则,我们就无法完成所有课程的学习。
时间复杂度 ,空间复杂度 。其中 和 分别为课程数和先修课程数。
class Solution { public boolean canFinish(int numCourses, int[][] prerequisites) { List<Integer>[] g = new List[numCourses]; Arrays.setAll(g, k -> new ArrayList<>()); int[] indeg = new int[numCourses]; for (var p : prerequisites) { int a = p[0], b = p[1]; g[b].add(a); ++indeg[a]; } Deque<Integer> q = new ArrayDeque<>(); for (int i = 0; i < numCourses; ++i) { if (indeg[i] == 0) { q.offer(i); } } int cnt = 0; while (!q.isEmpty()) { int i = q.poll(); ++cnt; for (int j : g[i]) { if (--indeg[j] == 0) { q.offer(j); } } } return cnt == numCourses; } }
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。void insert(String word) 向前缀树中插入字符串 word 。boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
提示:
1 <= word.length, prefix.length <= 2000word 和 prefix 仅由小写英文字母组成insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次方法一:前缀树
前缀树每个节点包括两部分:
我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 为真,则说明字典树中存在该字符串。
class Trie { private Trie[] children; private boolean isEnd; public Trie() { children = new Trie[26]; } public void insert(String word) { Trie node = this; for (char c : word.toCharArray()) { int idx = c - 'a'; if (node.children[idx] == null) { node.children[idx] = new Trie(); } node = node.children[idx]; } node.isEnd = true; } public boolean search(String word) { Trie node = searchPrefix(word); return node != null && node.isEnd; } public boolean startsWith(String prefix) { Trie node = searchPrefix(prefix); return node != null; } private Trie searchPrefix(String s) { Trie node = this; for (char c : s.toCharArray()) { int idx = c - 'a'; if (node.children[idx] == null) { return null; } node = node.children[idx]; } return node; } } /** * Your Trie object will be instantiated and called as such: * Trie obj = new Trie(); * obj.insert(word); * boolean param_2 = obj.search(word); * boolean param_3 = obj.startsWith(prefix); */
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4] 输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
提示:
1 <= target <= 1091 <= nums.length <= 1051 <= nums[i] <= 105进阶:
O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。方法一:前缀和 + 二分查找
我们先预处理出数组 的前缀和数组 ,其中 表示数组 前 项元素之和。由于数组 中的元素都是正整数,因此数组 也是单调递增的。另外,我们初始化答案 ,其中 为数组 的长度。
接下来,我们遍历前缀和数组 ,对于其中的每个元素 ,我们可以通过二分查找的方法找到满足 的最小下标 ,如果 ,则说明存在满足条件的子数组,我们可以更新答案,即 。
最后,如果 ,则说明存在满足条件的子数组,返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
方法二:双指针
我们可以使用双指针 和 维护一个窗口,其中窗口中的所有元素之和小于 。初始时 ,答案 ,其中 为数组 的长度。
接下来,指针 从 开始向右移动,每次移动一步,我们将指针 对应的元素加入窗口,同时更新窗口中元素之和。如果窗口中元素之和大于等于 ,说明当前子数组满足条件,我们可以更新答案,即 。然后我们不断地从窗口中移除元素 ,直到窗口中元素之和小于 ,然后重复上述过程。
最后,如果 ,则说明存在满足条件的子数组,返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
class Solution { public int minSubArrayLen(int target, int[] nums) { int n = nums.length; long[] s = new long[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } int ans = n + 1; for (int i = 0; i <= n; ++i) { int j = search(s, s[i] + target); if (j <= n) { ans = Math.min(ans, j - i); } } return ans <= n ? ans : 0; } private int search(long[] nums, long x) { int l = 0, r = nums.length; while (l < r) { int mid = (l + r) >> 1; if (nums[mid] >= x) { r = mid; } else { l = mid + 1; } } return l; } }
class Solution { public int minSubArrayLen(int target, int[] nums) { int n = nums.length; long s = 0; int ans = n + 1; for (int i = 0, j = 0; i < n; ++i) { s += nums[i]; while (j < n && s >= target) { ans = Math.min(ans, i - j + 1); s -= nums[j++]; } } return ans <= n ? ans : 0; } }
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]] 输出:[0,1] 解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 输出:[0,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
示例 3:
输入:numCourses = 1, prerequisites = [] 输出:[0]
提示:
1 <= numCourses <= 20000 <= prerequisites.length <= numCourses * (numCourses - 1)prerequisites[i].length == 20 <= ai, bi < numCoursesai != bi[ai, bi] 互不相同方法一:拓扑排序
对于本题,我们可以将课程看作图中的节点,先修课程看作图中的边,那么我们可以将本题转化为判断有向图中是否存在环。
具体地,我们可以使用拓扑排序的思想,对于每个入度为 的节点,我们将其出度的节点的入度减 ,直到所有节点都被遍历到。
如果所有节点都被遍历到,说明图中不存在环,那么我们就可以完成所有课程的学习;否则,我们就无法完成所有课程的学习。
时间复杂度 ,空间复杂度 。其中 和 分别为课程数和先修课程数。
class Solution { public int[] findOrder(int numCourses, int[][] prerequisites) { List<Integer>[] g = new List[numCourses]; Arrays.setAll(g, k -> new ArrayList<>()); int[] indeg = new int[numCourses]; for (var p : prerequisites) { int a = p[0], b = p[1]; g[b].add(a); ++indeg[a]; } Deque<Integer> q = new ArrayDeque<>(); for (int i = 0; i < numCourses; ++i) { if (indeg[i] == 0) { q.offer(i); } } int[] ans = new int[numCourses]; int cnt = 0; while (!q.isEmpty()) { int i = q.poll(); ans[cnt++] = i; for (int j : g[i]) { if (--indeg[j] == 0) { q.offer(j); } } } return cnt == numCourses ? ans : new int[0]; } }
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary() 初始化词典对象void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。示例:
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True
提示:
1 <= word.length <= 25addWord 中的 word 由小写英文字母组成search 中的 word 由 '.' 或小写英文字母组成104 次 addWord 和 search“前缀树”实现。
class Trie { Trie[] children = new Trie[26]; boolean isEnd; } class WordDictionary { private Trie trie; /** Initialize your data structure here. */ public WordDictionary() { trie = new Trie(); } public void addWord(String word) { Trie node = trie; for (char c : word.toCharArray()) { int idx = c - 'a'; if (node.children[idx] == null) { node.children[idx] = new Trie(); } node = node.children[idx]; } node.isEnd = true; } public boolean search(String word) { return search(word, trie); } private boolean search(String word, Trie node) { for (int i = 0; i < word.length(); ++i) { char c = word.charAt(i); int idx = c - 'a'; if (c != '.' && node.children[idx] == null) { return false; } if (c == '.') { for (Trie child : node.children) { if (child != null && search(word.substring(i + 1), child)) { return true; } } return false; } node = node.children[idx]; } return node.isEnd; } } /** * Your WordDictionary object will be instantiated and called as such: * WordDictionary obj = new WordDictionary(); * obj.addWord(word); * boolean param_2 = obj.search(word); */
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words, 返回所有二维网格上的单词 。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 1:
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"] 输出:["eat","oath"]
示例 2:
输入:board = [["a","b"],["c","d"]], words = ["abcb"] 输出:[]
提示:
m == board.lengthn == board[i].length1 <= m, n <= 12board[i][j] 是一个小写英文字母1 <= words.length <= 3 * 1041 <= words[i].length <= 10words[i] 由小写英文字母组成words 中的所有字符串互不相同方法一:前缀树 + DFS
我们首先将 words 中的单词构建成前缀树,前缀树的每个节点包含一个长度为 的数组 children,表示该节点的子节点,数组的下标表示子节点对应的字符,数组的值表示子节点的引用。同时,每个节点还包含一个整数 ref,表示该节点对应的单词在 words 中的引用,如果该节点不是单词的结尾,则 ref 的值为 。
接下来,我们对于 board 中的每个单元格,从该单元格出发,进行深度优先搜索,搜索过程中,如果当前单词不是前缀树中的单词,则剪枝,如果当前单词是前缀树中的单词,则将该单词加入答案,并将该单词在前缀树中的引用置为 ,表示该单词已经被找到,不需要再次搜索。
最后,我们将答案返回即可。
时间复杂度 ,空间复杂度 。其中 和 分别是 board 的行数和列数。而 和 分别是 words 中的单词的平均长度和单词的个数。
class Trie { Trie[] children = new Trie[26]; int ref = -1; public void insert(String w, int ref) { Trie node = this; for (int i = 0; i < w.length(); ++i) { int j = w.charAt(i) - 'a'; if (node.children[j] == null) { node.children[j] = new Trie(); } node = node.children[j]; } node.ref = ref; } } class Solution { private char[][] board; private String[] words; private List<String> ans = new ArrayList<>(); public List<String> findWords(char[][] board, String[] words) { this.board = board; this.words = words; Trie tree = new Trie(); for (int i = 0; i < words.length; ++i) { tree.insert(words[i], i); } int m = board.length, n = board[0].length; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { dfs(tree, i, j); } } return ans; } private void dfs(Trie node, int i, int j) { int idx = board[i][j] - 'a'; if (node.children[idx] == null) { return; } node = node.children[idx]; if (node.ref != -1) { ans.add(words[node.ref]); node.ref = -1; } char c = board[i][j]; board[i][j] = '#'; int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i + dirs[k], y = j + dirs[k + 1]; if (x >= 0 && x < board.length && y >= 0 && y < board[0].length && board[x][y] != '#') { dfs(node, x, y); } } board[i][j] = c; } }
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3] 输出:3
提示:
1 <= nums.length <= 1000 <= nums[i] <= 1000环状排列意味着第一个房屋和最后一个房屋中最多只能选择一个偷窃,因此可以把此环状排列房间问题约化为两个单排排列房屋子问题。
class Solution { public int rob(int[] nums) { int n = nums.length; if (n == 1) { return nums[0]; } int s1 = robRange(nums, 0, n - 2); int s2 = robRange(nums, 1, n - 1); return Math.max(s1, s2); } private int robRange(int[] nums, int l, int r) { int a = 0, b = nums[l]; for (int i = l + 1; i <= r; ++i) { int c = Math.max(nums[i] + a, b); a = b; b = c; } return b; } }
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入:s = "aacecaaa" 输出:"aaacecaaa"
示例 2:
输入:s = "abcd" 输出:"dcbabcd"
提示:
0 <= s.length <= 5 * 104s 仅由小写英文字母组成方法一:字符串哈希
字符串哈希是把一个任意长度的字符串映射成一个非负整数,并且其冲突的概率几乎为 0。字符串哈希用于计算字符串哈希值,快速判断两个字符串是否相等。
取一固定值 BASE,把字符串看作是 BASE 进制数,并分配一个大于 0 的数值,代表每种字符。一般来说,我们分配的数值都远小于 BASE。例如,对于小写字母构成的字符串,可以令 a=1, b=2, ..., z=26。取一固定值 MOD,求出该 BASE 进制对 M 的余数,作为该字符串的 hash 值。
一般来说,取 BASE=131 或者 BASE=13331,此时 hash 值产生的冲突概率极低。只要两个字符串 hash 值相同,我们就认为两个字符串是相等的。通常 MOD 取 2^64,C++ 里,可以直接使用 unsigned long long 类型存储这个 hash 值,在计算时不处理算术溢出问题,产生溢出时相当于自动对 2^64 取模,这样可以避免低效取模运算。
除了在极特殊构造的数据上,上述 hash 算法很难产生冲突,一般情况下上述 hash 算法完全可以出现在题目的标准答案中。我们还可以多取一些恰当的 BASE 和 MOD 的值(例如大质数),多进行几组 hash 运算,当结果都相同时才认为原字符串相等,就更加难以构造出使这个 hash 产生错误的数据。
对于本题,问题等价于找到字符串 s 的最长回文前缀。
记 s 的长度为 n,其最长回文前缀的长度为 m,将 s 的后 n-m 个字符反序并添加到 s 的前面即可构成最短回文串。
class Solution { public String shortestPalindrome(String s) { int base = 131; int mul = 1; int mod = (int) 1e9 + 7; int prefix = 0, suffix = 0; int idx = 0; int n = s.length(); for (int i = 0; i < n; ++i) { int t = s.charAt(i) - 'a' + 1; prefix = (int) (((long) prefix * base + t) % mod); suffix = (int) ((suffix + (long) t * mul) % mod); mul = (int) (((long) mul * base) % mod); if (prefix == suffix) { idx = i + 1; } } if (idx == n) { return s; } return new StringBuilder(s.substring(idx)).reverse().toString() + s; } }
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 105-104 <= nums[i] <= 104方法一:排序
将数组 升序排列,然后获取 。
时间复杂度 ,其中 表示数组 的长度。
方法二:partition
并不是所有时候,都需要整个数组进入有序状态,只需要局部有序,或者说,从大到小排序,只要 位置的元素有序,那么就能确定结果,此处使用快速排序。
快速排序有一特点,每一次循环结束时,能够确定的是: 一定处于它该处于的索引位置。从而根据它得知,结果值是在左数组还是在右数组当中,然后对那一数组进行排序即可。
时间复杂度 ,其中 表示数组 的长度。
class Solution { public int findKthLargest(int[] nums, int k) { int n = nums.length; return quickSort(nums, 0, n - 1, n - k); } private int quickSort(int[] nums, int left, int right, int k) { if (left == right) { return nums[left]; } int i = left - 1, j = right + 1; int x = nums[(left + right) >>> 1]; while (i < j) { while (nums[++i] < x) ; while (nums[--j] > x) ; if (i < j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } } if (j < k) { return quickSort(nums, j + 1, right, k); } return quickSort(nums, left, j, k); } }
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 91 <= n <= 60方法一:剪枝 + 回溯(两种方式)
我们设计一个函数 ,表示当前枚举到数字 ,还剩下和为 的数字需要枚举,当前搜索路径为 ,答案为 。
函数 的执行逻辑如下:
方式一:
方式二:
在主函数中,我们调用 ,即从数字 开始枚举,剩下和为 的数字需要枚举。搜索完成后,即可得到所有的答案。
时间复杂度 ,空间复杂度 。
方法二:二进制枚举
我们可以用一个长度为 的二进制整数表示数字 到 的选取情况,其中二进制整数的第 位表示数字 是否被选取,如果第 位为 ,则表示数字 被选取,否则表示数字 没有被选取。
我们在 范围内枚举二进制整数,对于当前枚举到的二进制整数 ,如果 的二进制表示中 的个数为 ,且 的二进制表示中 所对应的数字之和为 ,则说明 对应的数字选取方案是一组答案。我们将 对应的数字选取方案加入答案即可。
时间复杂度 ,空间复杂度 。
相似题目:
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int k; public List<List<Integer>> combinationSum3(int k, int n) { this.k = k; dfs(1, n); return ans; } private void dfs(int i, int s) { if (s == 0) { if (t.size() == k) { ans.add(new ArrayList<>(t)); } return; } if (i > 9 || i > s || t.size() >= k) { return; } t.add(i); dfs(i + 1, s - i); t.remove(t.size() - 1); dfs(i + 1, s); } }
class Solution { private List<List<Integer>> ans = new ArrayList<>(); private List<Integer> t = new ArrayList<>(); private int k; public List<List<Integer>> combinationSum3(int k, int n) { this.k = k; dfs(1, n); return ans; } private void dfs(int i, int s) { if (s == 0) { if (t.size() == k) { ans.add(new ArrayList<>(t)); } return; } if (i > 9 || i > s || t.size() >= k) { return; } for (int j = i; j <= 9; ++j) { t.add(j); dfs(j + 1, s - j); t.remove(t.size() - 1); } } }
class Solution { public List<List<Integer>> combinationSum3(int k, int n) { List<List<Integer>> ans = new ArrayList<>(); for (int mask = 0; mask < 1 << 9; ++mask) { if (Integer.bitCount(mask) == k) { List<Integer> t = new ArrayList<>(); int s = 0; for (int i = 0; i < 9; ++i) { if ((mask >> i & 1) == 1) { s += (i + 1); t.add(i + 1); } } if (s == n) { ans.add(t); } } } return ans; } }
给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。
示例 1:
输入:nums = [1,2,3,1] 输出:true
示例 2:
输入:nums = [1,2,3,4] 输出:false
示例 3:
输入:nums = [1,1,1,3,3,4,3,2,4,2] 输出:true
提示:
1 <= nums.length <= 105-109 <= nums[i] <= 109方法一:排序
我们先对数组 nums 进行排序。
然后遍历数组,如果存在相邻两个元素相同,说明数组中存在重复元素,直接返回 true。
否则,遍历结束,返回 false。
时间复杂度 。其中 是数组 nums 的长度。
方法二:哈希表
遍历数组,将出现过的元素记录在哈希表 中。若元素第二次出现时,说明数组中存在重复元素,直接返回 true。
时间复杂度 ,空间复杂度 。其中 是数组 nums 的长度。
class Solution { public boolean containsDuplicate(int[] nums) { Arrays.sort(nums); for (int i = 0; i < nums.length - 1; ++i) { if (nums[i] == nums[i + 1]) { return true; } } return false; } }
class Solution { public boolean containsDuplicate(int[] nums) { Set<Integer> s = new HashSet<>(); for (int num : nums) { if (!s.add(num)) { return true; } } return false; } }
城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:
lefti 是第 i 座建筑物左边缘的 x 坐标。righti 是第 i 座建筑物右边缘的 x 坐标。heighti 是第 i 座建筑物的高度。你可以假设所有的建筑都是完美的长方形,在高度为 0 的绝对平坦的表面上。
天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]
示例 1:
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]] 输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]] 解释: 图 A 显示输入的所有建筑物的位置和高度, 图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
示例 2:
输入:buildings = [[0,2,3],[2,5,3]] 输出:[[0,3],[5,0]]
提示:
1 <= buildings.length <= 1040 <= lefti < righti <= 231 - 11 <= heighti <= 231 - 1buildings 按 lefti 非递减排序方法一:扫描线+优先队列
记录下所有建筑物的左右边界线,升序排序之后得到序列 lines。对于每一个边界线 lines[i],找出所有包含 lines[i] 的建筑物,并确保建筑物的左边界小于等于 lines[i],右边界大于 lines[i],则这些建筑物中高度最高的建筑物的高度就是该线轮廓点的高度。可以使用建筑物的高度构建优先队列(大根堆),同时需要注意高度相同的轮廓点需要合并为一个。
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,1], k = 3 输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1 输出:true
示例 3:
输入:nums = [1,2,3,1,2,3], k = 2 输出:false
提示:
1 <= nums.length <= 105-109 <= nums[i] <= 1090 <= k <= 105方法一:哈希表
我们用哈希表 存放最近遍历到的数以及对应的下标。
遍历数组 nums,对于当前遍历到的元素 ,如果在哈希表中存在,并且下标与当前元素的下标之差不超过 ,则返回 true,否则将当前元素加入哈希表中。
遍历结束后,返回 false。
时间复杂度 ,空间复杂度 。其中 为数组 nums 的长度。
class Solution { public boolean containsNearbyDuplicate(int[] nums, int k) { Map<Integer, Integer> d = new HashMap<>(); for (int i = 0; i < nums.length; ++i) { if (i - d.getOrDefault(nums[i], -1000000) <= k) { return true; } d.put(nums[i], i); } return false; } }
给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。
示例 1:
输入:nums = [1,2,3,1], k = 3, t = 0 输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1, t = 2 输出:true
示例 3:
输入:nums = [1,5,9,1,5,9], k = 2, t = 3 输出:false
提示:
0 <= nums.length <= 2 * 104-231 <= nums[i] <= 231 - 10 <= k <= 1040 <= t <= 231 - 1方法一:滑动窗口 + 有序集合
维护一个大小为 的滑动窗口,窗口中的元素保持有序。
遍历数组 nums,对于每个元素 ,我们在有序集合中查找第一个大于等于 的元素,如果元素存在,并且该元素小于等于 ,说明找到了一对符合条件的元素,返回 true。否则,我们将 插入到有序集合中,并且如果有序集合的大小超过了 ,我们需要将最早加入有序集合的元素删除。
时间复杂度 ,其中 是数组 nums 的长度。对于每个元素,我们需要 的时间来查找有序集合中的元素,一共有 个元素,因此总时间复杂度是 。
class Solution { public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) { TreeSet<Long> ts = new TreeSet<>(); for (int i = 0; i < nums.length; ++i) { Long x = ts.ceiling((long) nums[i] - (long) valueDiff); if (x != null && x <= (long) nums[i] + (long) valueDiff) { return true; } ts.add((long) nums[i]); if (i >= indexDiff) { ts.remove((long) nums[i - indexDiff]); } } return false; } }
在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:4
示例 2:
输入:matrix = [["0","1"],["1","0"]] 输出:1
示例 3:
输入:matrix = [["0"]] 输出:0
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 300matrix[i][j] 为 '0' 或 '1'方法一:动态规划
我们定义 表示以下标 作为正方形右下角的最大正方形边长。答案为所有 中的最大值。
状态转移方程为:
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
class Solution { public int maximalSquare(char[][] matrix) { int m = matrix.length, n = matrix[0].length; int[][] dp = new int[m + 1][n + 1]; int mx = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (matrix[i][j] == '1') { dp[i + 1][j + 1] = Math.min(Math.min(dp[i][j + 1], dp[i + 1][j]), dp[i][j]) + 1; mx = Math.max(mx, dp[i + 1][j + 1]); } } } return mx * mx; } }
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例 1:
输入:root = [1,2,3,4,5,6] 输出:6
示例 2:
输入:root = [] 输出:0
示例 3:
输入:root = [1] 输出:1
提示:
[0, 5 * 104]0 <= Node.val <= 5 * 104进阶:遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?
方法一:递归
递归遍历整棵树,统计结点个数。
时间复杂度 ,空间复杂度 。其中 为树的结点个数。
方法二:二分查找
对于此题,我们还可以利用完全二叉树的特点,设计一个更快的算法。
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
若满二叉树的层数为 ,则总结点数为 。
我们可以先对 的左右子树进行高度统计,分别记为 和 。
时间复杂度 。
class Solution { public int countNodes(TreeNode root) { if (root == null) { return 0; } return 1 + countNodes(root.left) + countNodes(root.right); } }
class Solution { public int countNodes(TreeNode root) { if (root == null) { return 0; } int left = depth(root.left); int right = depth(root.right); if (left == right) { return (1 << left) + countNodes(root.right); } return (1 << right) + countNodes(root.left); } private int depth(TreeNode root) { int d = 0; for (; root != null; root = root.left) { ++d; } return d; } }
给你 二维 平面上两个 由直线构成且边与坐标轴平行/垂直 的矩形,请你计算并返回两个矩形覆盖的总面积。
每个矩形由其 左下 顶点和 右上 顶点坐标表示:
(ax1, ay1) 和右上顶点 (ax2, ay2) 定义。(bx1, by1) 和右上顶点 (bx2, by2) 定义。示例 1:
输入:ax1 = -3, ay1 = 0, ax2 = 3, ay2 = 4, bx1 = 0, by1 = -1, bx2 = 9, by2 = 2 输出:45
示例 2:
输入:ax1 = -2, ay1 = -2, ax2 = 2, ay2 = 2, bx1 = -2, by1 = -2, bx2 = 2, by2 = 2 输出:16
提示:
-104 <= ax1, ay1, ax2, ay2, bx1, by1, bx2, by2 <= 104方法一:计算重叠面积
我们先计算出两个矩形各自的面积,记为 和 ,然后计算重叠的宽度 和高度 ,那么重叠的面积为 ,最后将 , 和重叠面积相减即可。
时间复杂度 ,空间复杂度 。
class Solution { public int computeArea(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2) { int a = (ax2 - ax1) * (ay2 - ay1); int b = (bx2 - bx1) * (by2 - by1); int width = Math.min(ax2, bx2) - Math.max(ax1, bx1); int height = Math.min(ay2, by2) - Math.max(ay1, by1); return a + b - Math.max(height, 0) * Math.max(width, 0); } }
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。
示例 1:
输入:s = "1 + 1" 输出:2
示例 2:
输入:s = " 2-1 + 2 " 输出:3
示例 3:
输入:s = "(1+(4+5+2)-3)+(6+8)" 输出:23
提示:
1 <= s.length <= 3 * 105s 由数字、'+'、'-'、'('、')'、和 ' ' 组成s 表示一个有效的表达式"+(2 + 3)" 无效)"-(2 + 3)" 是有效的)方法一:栈
我们用一个栈 来保存当前的计算结果和操作符,用一个变量 保存当前的符号,变量 保存最终的计算结果。
接下来,我们遍历字符串 的每一个字符:
'+',我们修改变量 为正号。'-',我们修改变量 为负号。'(',我们把当前的 和 入栈,并分别置空置 1,重新开始计算新的 和 。')',我们弹出栈顶的两个元素,一个是操作符,一个是括号前计算好的数字,我们将当前的数字乘上操作符,再加上之前的数字,作为新的 。遍历完字符串 之后,我们返回 。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
class Solution { public int calculate(String s) { Deque<Integer> stk = new ArrayDeque<>(); int sign = 1; int ans = 0; int n = s.length(); for (int i = 0; i < n; ++i) { char c = s.charAt(i); if (Character.isDigit(c)) { int j = i; int x = 0; while (j < n && Character.isDigit(s.charAt(j))) { x = x * 10 + s.charAt(j) - '0'; j++; } ans += sign * x; i = j - 1; } else if (c == '+') { sign = 1; } else if (c == '-') { sign = -1; } else if (c == '(') { stk.push(ans); stk.push(sign); ans = 0; sign = 1; } else if (c == ')') { ans = stk.pop() * ans + stk.pop(); } } return ans; } }
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。int pop() 移除并返回栈顶元素。int top() 返回栈顶元素。boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。注意:
push to back、peek/pop from front、size 和 is empty 这些操作。示例:
输入: ["MyStack", "push", "push", "top", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 2, 2, false] 解释: MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // 返回 2 myStack.pop(); // 返回 2 myStack.empty(); // 返回 False
提示:
1 <= x <= 9100 次 push、pop、top 和 emptypop 和 top 都保证栈不为空进阶:你能否仅用一个队列来实现栈。
方法一:两个队列
我们使用两个队列 和 ,其中 用于存储栈中的元素,而 用于辅助实现栈的操作。
push 操作:将元素压入 ,然后将 中的元素依次弹出并压入 ,最后交换 和 的引用。时间复杂度 。pop 操作:直接弹出 的队首元素。时间复杂度 。top 操作:直接返回 的队首元素。时间复杂度 。empty 操作:判断 是否为空。时间复杂度 。空间复杂度 ,其中 是栈中元素的个数。
import java.util.Deque; class MyStack { private Deque<Integer> q1 = new ArrayDeque<>(); private Deque<Integer> q2 = new ArrayDeque<>(); public MyStack() { } public void push(int x) { q2.offer(x); while (!q1.isEmpty()) { q2.offer(q1.poll()); } Deque<Integer> q = q1; q1 = q2; q2 = q; } public int pop() { return q1.poll(); } public int top() { return q1.peek(); } public boolean empty() { return q1.isEmpty(); } } /** * Your MyStack object will be instantiated and called as such: * MyStack obj = new MyStack(); * obj.push(x); * int param_2 = obj.pop(); * int param_3 = obj.top(); * boolean param_4 = obj.empty(); */
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:

输入:root = [4,2,7,1,3,6,9] 输出:[4,7,2,9,6,3,1]
示例 2:

输入:root = [2,1,3] 输出:[2,3,1]
示例 3:
输入:root = [] 输出:[]
提示:
[0, 100] 内-100 <= Node.val <= 100方法一:递归
递归的思路很简单,就是交换当前节点的左右子树,然后递归地交换当前节点的左右子树。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { public TreeNode invertTree(TreeNode root) { dfs(root); return root; } private void dfs(TreeNode root) { if (root == null) { return; } TreeNode t = root.left; root.left = root.right; root.right = t; dfs(root.left); dfs(root.right); } }
class Solution { public TreeNode invertTree(TreeNode root) { if (root == null) { return null; } TreeNode l = invertTree(root.left); TreeNode r = invertTree(root.right); root.left = r; root.right = l; return root; } }
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。
你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。
示例 1:
输入:s = "3+2*2" 输出:7
示例 2:
输入:s = " 3/2 " 输出:1
示例 3:
输入:s = " 3+5 / 2 " 输出:5
提示:
1 <= s.length <= 3 * 105s 由整数和算符 ('+', '-', '*', '/') 组成,中间由一些空格隔开s 表示一个 有效表达式[0, 231 - 1] 内方法一:栈
遍历字符串 ,并用变量 sign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 sign 来决定计算方式:
遍历结束后,将栈中元素求和即为答案。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
class Solution { public int calculate(String s) { Deque<Integer> stk = new ArrayDeque<>(); char sign = '+'; int v = 0; for (int i = 0; i < s.length(); ++i) { char c = s.charAt(i); if (Character.isDigit(c)) { v = v * 10 + (c - '0'); } if (i == s.length() - 1 || c == '+' || c == '-' || c == '*' || c == '/') { if (sign == '+') { stk.push(v); } else if (sign == '-') { stk.push(-v); } else if (sign == '*') { stk.push(stk.pop() * v); } else { stk.push(stk.pop() / v); } sign = c; v = 0; } } int ans = 0; while (!stk.isEmpty()) { ans += stk.pop(); } return ans; } }
给定一个 无重复元素 的 有序 整数数组 nums 。
返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。
列表中的每个区间范围 [a,b] 应该按如下格式输出:
"a->b" ,如果 a != b"a" ,如果 a == b示例 1:
输入:nums = [0,1,2,4,5,7] 输出:["0->2","4->5","7"] 解释:区间范围是: [0,2] --> "0->2" [4,5] --> "4->5" [7,7] --> "7"
示例 2:
输入:nums = [0,2,3,4,6,8,9] 输出:["0","2->4","6","8->9"] 解释:区间范围是: [0,0] --> "0" [2,4] --> "2->4" [6,6] --> "6" [8,9] --> "8->9"
提示:
0 <= nums.length <= 20-231 <= nums[i] <= 231 - 1nums 中的所有值都 互不相同nums 按升序排列方法一:双指针
我们可以用双指针 和 找出每个区间的左右端点。
遍历数组,当 且 时, 向右移动,否则区间 已经找到,将其加入答案,然后将 移动到 的位置,继续寻找下一个区间。
时间复杂度 ,其中 为数组长度。空间复杂度 。
class Solution { public List<String> summaryRanges(int[] nums) { List<String> ans = new ArrayList<>(); for (int i = 0, j, n = nums.length; i < n; i = j + 1) { j = i; while (j + 1 < n && nums[j + 1] == nums[j] + 1) { ++j; } ans.add(f(nums, i, j)); } return ans; } private String f(int[] nums, int i, int j) { return i == j ? nums[i] + "" : String.format("%d->%d", nums[i], nums[j]); } }
给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。
示例 1:
输入:nums = [3,2,3] 输出:[3]
示例 2:
输入:nums = [1] 输出:[1]
示例 3:
输入:nums = [1,2] 输出:[1,2]
提示:
1 <= nums.length <= 5 * 104-109 <= nums[i] <= 109进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1)的算法解决此问题。
摩尔投票法。
class Solution { public List<Integer> majorityElement(int[] nums) { int n1 = 0, n2 = 0; int m1 = 0, m2 = 1; for (int m : nums) { if (m == m1) { ++n1; } else if (m == m2) { ++n2; } else if (n1 == 0) { m1 = m; ++n1; } else if (n2 == 0) { m2 = m; ++n2; } else { --n1; --n2; } } List<Integer> ans = new ArrayList<>(); n1 = 0; n2 = 0; for (int m : nums) { if (m == m1) { ++n1; } else if (m == m2) { ++n2; } } if (n1 > nums.length / 3) { ans.add(m1); } if (n2 > nums.length / 3) { ans.add(m2); } return ans; } }
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1 输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3 输出:3
提示:
n 。1 <= k <= n <= 1040 <= Node.val <= 104进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
方法一:中序遍历
由于二叉搜索树的性质,中序遍历一定能得到升序序列,因此可以采用中序遍历找出第 k 小的元素。
方法二:预处理结点数
预处理每个结点作为根节点的子树的节点数。
这种算法可以用来优化频繁查找第 k 个树、而二叉搜索树本身不被修改的情况。
class Solution { public int kthSmallest(TreeNode root, int k) { Deque<TreeNode> stk = new ArrayDeque<>(); while (root != null || !stk.isEmpty()) { if (root != null) { stk.push(root); root = root.left; } else { root = stk.pop(); if (--k == 0) { return root.val; } root = root.right; } } return 0; } }
class Solution { public int kthSmallest(TreeNode root, int k) { BST bst = new BST(root); return bst.kthSmallest(k); } } class BST { private TreeNode root; private Map<TreeNode, Integer> cnt = new HashMap<>(); public BST(TreeNode root) { this.root = root; count(root); } public int kthSmallest(int k) { TreeNode node = root; while (node != null) { int v = node.left == null ? 0 : cnt.get(node.left); if (v == k - 1) { return node.val; } if (v < k - 1) { node = node.right; k -= (v + 1); } else { node = node.left; } } return 0; } private int count(TreeNode root) { if (root == null) { return 0; } int n = 1 + count(root.left) + count(root.right); cnt.put(root, n); return n; } }
给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。
如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。
示例 1:
输入:n = 1 输出:true 解释:20 = 1
示例 2:
输入:n = 16 输出:true 解释:24 = 16
示例 3:
输入:n = 3 输出:false
示例 4:
输入:n = 4 输出:true
示例 5:
输入:n = 5 输出:false
提示:
-231 <= n <= 231 - 1进阶:你能够不使用循环/递归解决此问题吗?
方法一:位运算
可将最后一个二进制形式的 的最后一位 移除,若移除后为 ,说明 是 的幂。
方法二:lowbit
可以得到 的最后一位 表示的十进制数,若与 相等,说明 是 的幂。
注意:要满足 是 的幂次方,需要保证 大于 。
lowbit:
class Solution { public boolean isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; } }
lowbit:
class Solution { public boolean isPowerOfTwo(int n) { return n > 0 && n == (n & (-n)); } }
lowbit:
lowbit:
lowbit:
lowbit:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾int pop() 从队列的开头移除并返回元素int peek() 返回队列开头的元素boolean empty() 如果队列为空,返回 true ;否则,返回 false说明:
push to top, peek/pop from top, size, 和 is empty 操作是合法的。示例 1:
输入: ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 1, 1, false] 解释: MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false
提示:
1 <= x <= 9100 次 push、pop、peek 和 emptypop 或者 peek 操作)进阶:
O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。方法一:双栈
使用两个栈,其中栈 stk1用于入队,另一个栈 stk2 用于出队。
入队时,直接将元素入栈 stk1。时间复杂度 。
出队时,先判断栈 stk2 是否为空,如果为空,则将栈 stk1 中的元素全部出栈并入栈 stk2,然后再从栈 stk2 中出栈一个元素。如果栈 stk2 不为空,则直接从栈 stk2 中出栈一个元素。均摊时间复杂度 。
获取队首元素时,先判断栈 stk2 是否为空,如果为空,则将栈 stk1 中的元素全部出栈并入栈 stk2,然后再从栈 stk2 中获取栈顶元素。如果栈 stk2 不为空,则直接从栈 stk2 中获取栈顶元素。均摊时间复杂度 。
判断队列是否为空时,只要判断两个栈是否都为空即可。时间复杂度 。
class MyQueue { private Deque<Integer> stk1 = new ArrayDeque<>(); private Deque<Integer> stk2 = new ArrayDeque<>(); public MyQueue() { } public void push(int x) { stk1.push(x); } public int pop() { move(); return stk2.pop(); } public int peek() { move(); return stk2.peek(); } public boolean empty() { return stk1.isEmpty() && stk2.isEmpty(); } private void move() { while (stk2.isEmpty()) { while (!stk1.isEmpty()) { stk2.push(stk1.pop()); } } } } /** * Your MyQueue object will be instantiated and called as such: * MyQueue obj = new MyQueue(); * obj.push(x); * int param_2 = obj.pop(); * int param_3 = obj.peek(); * boolean param_4 = obj.empty(); */
给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。
示例 1:
输入:n = 13 输出:6
示例 2:
输入:n = 0 输出:0
提示:
0 <= n <= 109方法一:数位 DP
这道题实际上是求在给定区间 中,数字中出现 个数。个数与数的位数以及每一位上的数字有关。我们可以用数位 DP 的思路来解决这道题。数位 DP 中,数的大小对复杂度的影响很小。
对于区间 问题,我们一般会将其转化为 然后再减去 的问题,即:
不过对于本题而言,我们只需要求出区间 的值即可。
这里我们用记忆化搜索来实现数位 DP。从起点向下搜索,到最底层得到方案数,一层层向上返回答案并累加,最后从搜索起点得到最终的答案。
基本步骤如下:
其中:
pos 表示数字的位数,从末位或者第一位开始,一般根据题目的数字构造性质来选择顺序。对于本题,我们选择从高位开始,因此,pos 的初始值为 len;cnt 表示当前数字中包含的 的个数。limit 表示可填的数字的限制,如果无限制,那么可以选择 ,否则,只能选择 。如果 limit 为 true 且已经取到了能取到的最大值,那么下一个 limit 同样为 true;如果 limit 为 true 但是还没有取到最大值,或者 limit 为 false,那么下一个 limit 为 false。关于函数的实现细节,可以参考下面的代码。
时间复杂度 。
相似题目:
class Solution { private int[] a = new int[12]; private int[][] dp = new int[12][12]; public int countDigitOne(int n) { int len = 0; while (n > 0) { a[++len] = n % 10; n /= 10; } for (var e : dp) { Arrays.fill(e, -1); } return dfs(len, 0, true); } private int dfs(int pos, int cnt, boolean limit) { if (pos <= 0) { return cnt; } if (!limit && dp[pos][cnt] != -1) { return dp[pos][cnt]; } int up = limit ? a[pos] : 9; int ans = 0; for (int i = 0; i <= up; ++i) { ans += dfs(pos - 1, cnt + (i == 1 ? 1 : 0), limit && i == up); } if (!limit) { dp[pos][cnt] = ans; } return ans; } }
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1] 输出:true
示例 2:
输入:head = [1,2] 输出:false
提示:
[1, 105] 内0 <= Node.val <= 9进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
方法一:快慢指针
我们可以先用快慢指针找到链表的中点,接着反转右半部分的链表。然后同时遍历前后两段链表,若前后两段链表节点对应的值不等,说明不是回文链表,否则说明是回文链表。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
class Solution { public boolean isPalindrome(ListNode head) { ListNode slow = head; ListNode fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } ListNode cur = slow.next; slow.next = null; ListNode pre = null; while (cur != null) { ListNode t = cur.next; cur.next = pre; pre = cur; cur = t; } while (pre != null) { if (pre.val != head.val) { return false; } pre = pre.next; head = head.next; } return true; } }
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 输出: 6 解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 输出: 2 解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
方法一:迭代或递归
从上到下搜索,找到第一个值位于 之间的结点即可。
既可以用迭代实现,也可以用递归实现。
迭代的时间复杂度为 ,空间复杂度为 。
递归的时间复杂度为 ,空间复杂度为 。
迭代:
递归:
迭代:
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { while (true) { if (root.val < Math.min(p.val, q.val)) { root = root.right; } else if (root.val > Math.max(p.val, q.val)) { root = root.left; } else { return root; } } } }
递归:
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root.val < Math.min(p.val, q.val)) { return lowestCommonAncestor(root.right, p, q); } if (root.val > Math.max(p.val, q.val)) { return lowestCommonAncestor(root.left, p, q); } return root; } }
迭代:
递归:
迭代:
递归:
迭代:
递归:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出:3 解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出:5 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2 输出:1
提示:
[2, 105] 内。-109 <= Node.val <= 109Node.val 互不相同 。p != qp 和 q 均存在于给定的二叉树中。方法一:递归
根据“最近公共祖先”的定义,若 root 是 p, q 的最近公共祖先 ,则只可能为以下情况之一:
lowestCommonAncestor(root.left, p, q);lowestCommonAncestor(root.right, p, q)。边界条件讨论:
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if (left == null) return right; if (right == null) return left; return root; } }
有一个单链表的 head,我们想删除它其中的一个节点 node。
给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
node 前面的所有值顺序相同。node 后面的所有值顺序相同。自定义测试:
head 和要给出的节点 node。node 不应该是链表的最后一个节点,而应该是链表中的一个实际节点。示例 1:
输入:head = [4,5,1,9], node = 5 输出:[4,1,9] 解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
示例 2:
输入:head = [4,5,1,9], node = 1 输出:[4,5,9] 解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9
提示:
[2, 1000]-1000 <= Node.val <= 1000node 是 链表中的节点 ,且 不是末尾节点方法一:节点赋值
我们可以将当前节点的值替换为下一个节点的值,然后删除下一个节点。这样就可以达到删除当前节点的目的。
时间复杂度 ,空间复杂度 。
class Solution { public void deleteNode(ListNode node) { node.val = node.next.val; node.next = node.next.next; } }
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4] 输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3] 输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 105-30 <= nums[i] <= 30nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
方法一:两次遍历
我们定义两个变量 和 ,分别表示当前元素左边所有元素的乘积和右边所有元素的乘积。初始时 , 。定义一个长度为 的答案数组 。
我们先从左到右遍历数组,对于遍历到的第 个元素,我们用 更新 ,然后 乘以 。
然后,我们从右到左遍历数组,对于遍历到的第 个元素,我们更新 为 ,然后 乘以 。
遍历结束后,数组 ans 即为所求的答案。
时间复杂度 ,其中 是数组 nums 的长度。忽略答案数组的空间消耗,空间复杂度 。
class Solution { public int[] productExceptSelf(int[] nums) { int n = nums.length; int[] ans = new int[n]; for (int i = 0, left = 1; i < n; ++i) { ans[i] = left; left *= nums[i]; } for (int i = n - 1, right = 1; i >= 0; --i) { ans[i] *= right; right *= nums[i]; } return ans; } }
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1 输出:[1]
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 1041 <= k <= nums.length方法一:优先队列(大根堆)
我们可以使用优先队列(大根堆)来维护滑动窗口中的最大值。
先将前 个元素加入优先队列,接下来从第 个元素开始,将新元素加入优先队列,同时判断堆顶元素是否滑出窗口,如果滑出窗口则将堆顶元素弹出。然后我们将堆顶元素加入结果数组。
时间复杂度 ,空间复杂度 。其中 为数组长度。
方法二:单调队列
这道题也可以使用单调队列来解决。时间复杂度 ,空间复杂度 。
单调队列常见模型:找出滑动窗口中的最大值/最小值。模板:
class Solution { public int[] maxSlidingWindow(int[] nums, int k) { PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]); int n = nums.length; for (int i = 0; i < k - 1; ++i) { q.offer(new int[] {nums[i], i}); } int[] ans = new int[n - k + 1]; for (int i = k - 1, j = 0; i < n; ++i) { q.offer(new int[] {nums[i], i}); while (q.peek()[1] <= i - k) { q.poll(); } ans[j++] = q.peek()[0]; } return ans; } }
class Solution { public int[] maxSlidingWindow(int[] nums, int k) { int n = nums.length; int[] ans = new int[n - k + 1]; Deque<Integer> q = new ArrayDeque<>(); for (int i = 0, j = 0; i < n; ++i) { if (!q.isEmpty() && i - k + 1 > q.peekFirst()) { q.pollFirst(); } while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) { q.pollLast(); } q.offer(i); if (i >= k - 1) { ans[j++] = nums[q.peekFirst()]; } } return ans; } }
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5 输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20 输出:false
提示:
m == matrix.lengthn == matrix[i].length1 <= n, m <= 300-109 <= matrix[i][j] <= 109-109 <= target <= 109方法一:二分查找
由于每一行的所有元素升序排列,因此,对于每一行,我们可以使用二分查找找到第一个大于等于 target 的元素,然后判断该元素是否等于 target。如果等于 target,说明找到了目标值,直接返回 true。如果不等于 target,说明这一行的所有元素都小于 target,应该继续搜索下一行。
如果所有行都搜索完了,都没有找到目标值,说明目标值不存在,返回 false。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵的行数和列数。
方法二:从左下角或右上角搜索
这里我们以左下角作为起始搜索点,往右上方向开始搜索,比较当前元素 matrix[i][j]与 target 的大小关系:
matrix[i][j] == target,说明找到了目标值,直接返回 true。matrix[i][j] > target,说明这一行从当前位置开始往右的所有元素均大于 target,应该让 指针往上移动,即 。matrix[i][j] < target,说明这一列从当前位置开始往上的所有元素均小于 target,应该让 指针往右移动,即 。若搜索结束依然找不到 target,返回 false。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵的行数和列数。
class Solution { public boolean searchMatrix(int[][] matrix, int target) { for (var row : matrix) { int j = Arrays.binarySearch(row, target); if (j >= 0) { return true; } } return false; } }
class Solution { public boolean searchMatrix(int[][] matrix, int target) { int m = matrix.length, n = matrix[0].length; int i = m - 1, j = 0; while (i >= 0 && j < n) { if (matrix[i][j] == target) { return true; } if (matrix[i][j] > target) { --i; } else { ++j; } } return false; } }
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104 。
示例 1:
输入:expression = "2-1-1" 输出:[0,2] 解释: ((2-1)-1) = 0 (2-(1-1)) = 2
示例 2:
输入:expression = "2*3-4*5" 输出:[-34,-14,-10,-10,10] 解释: (2*(3-(4*5))) = -34 ((2*3)-(4*5)) = -14 ((2*(3-4))*5) = -10 (2*((3-4)*5)) = -10 (((2*3)-4)*5) = 10
提示:
1 <= expression.length <= 20expression 由数字和算符 '+'、'-' 和 '*' 组成。[0, 99] 方法一:记忆化搜索
class Solution { private static Map<String, List<Integer>> memo = new HashMap<>(); public List<Integer> diffWaysToCompute(String expression) { return dfs(expression); } private List<Integer> dfs(String exp) { if (memo.containsKey(exp)) { return memo.get(exp); } List<Integer> ans = new ArrayList<>(); if (exp.length() < 3) { ans.add(Integer.parseInt(exp)); return ans; } for (int i = 0; i < exp.length(); ++i) { char c = exp.charAt(i); if (c == '-' || c == '+' || c == '*') { List<Integer> left = dfs(exp.substring(0, i)); List<Integer> right = dfs(exp.substring(i + 1)); for (int a : left) { for (int b : right) { if (c == '-') { ans.add(a - b); } else if (c == '+') { ans.add(a + b); } else { ans.add(a * b); } } } } } memo.put(exp, ans); return ans; } }
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram" 输出: true
示例 2:
输入: s = "rat", t = "car" 输出: false
提示:
1 <= s.length, t.length <= 5 * 104s 和 t 仅包含小写字母进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
方法一:计数
我们先判断两个字符串的长度是否相等,如果不相等,说明两个字符串中的字符肯定不同,返回 false。
否则,我们用哈希表或者一个长度为 的数组来记录字符串 中每个字符出现的次数,然后遍历另一个字符串 ,每遍历到一个字符,就将哈希表中对应的字符次数减一,如果减一后的次数小于 ,说明该字符在两个字符串中出现的次数不同,返回 false。如果遍历完两个字符串后,哈希表中的所有字符次数都为 ,说明两个字符串中的字符出现次数相同,返回 true。
时间复杂度 ,空间复杂度 ,其中 是字符串的长度;而 是字符集的大小,本题中 。
class Solution { public boolean isAnagram(String s, String t) { if (s.length() != t.length()) { return false; } int[] cnt = new int[26]; for (int i = 0; i < s.length(); ++i) { ++cnt[s.charAt(i) - 'a']; --cnt[t.charAt(i) - 'a']; } for (int i = 0; i < 26; ++i) { if (cnt[i] != 0) { return false; } } return true; } }
给定一个字符串数组 wordDict 和两个已经存在于该数组中的不同的字符串 word1 和 word2 。返回列表中这两个单词之间的最短距离。
示例 1:
输入: wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "coding", word2 = "practice" 输出: 3
示例 2:
输入: wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "coding" 输出: 1
提示:
1 <= wordsDict.length <= 3 * 1041 <= wordsDict[i].length <= 10wordsDict[i] 由小写英文字母组成word1 和 word2 在 wordsDict 中word1 != word2方法一:双指针
遍历数组 wordsDict,找到 word1 和 word2 的下标 和 ,求 的最小值。
时间复杂度 ,空间复杂度 。其中 为数组 wordsDict 的长度。
class Solution { public int shortestDistance(String[] wordsDict, String word1, String word2) { int ans = 0x3f3f3f3f; for (int k = 0, i = -1, j = -1; k < wordsDict.length; ++k) { if (wordsDict[k].equals(word1)) { i = k; } if (wordsDict[k].equals(word2)) { j = k; } if (i != -1 && j != -1) { ans = Math.min(ans, Math.abs(i - j)); } } return ans; } }
请设计一个类,使该类的构造函数能够接收一个字符串数组。然后再实现一个方法,该方法能够分别接收两个单词,并返回列表中这两个单词之间的最短距离。
实现 WordDistanc 类:
WordDistance(String[] wordsDict) 用字符串数组 wordsDict 初始化对象。int shortest(String word1, String word2) 返回数组 worddict 中 word1 和 word2 之间的最短距离。示例 1:
输入:
["WordDistance", "shortest", "shortest"]
[[["practice", "makes", "perfect", "coding", "makes"]], ["coding", "practice"], ["makes", "coding"]]
输出:
[null, 3, 1]
解释:
WordDistance wordDistance = new WordDistance(["practice", "makes", "perfect", "coding", "makes"]);
wordDistance.shortest("coding", "practice"); // 返回 3
wordDistance.shortest("makes", "coding"); // 返回 1
注意:
1 <= wordsDict.length <= 3 * 1041 <= wordsDict[i].length <= 10wordsDict[i] 由小写英文字母组成word1 和 word2 在数组 wordsDict 中word1 != word2shortest 操作次数不大于 5000 方法一:哈希表 + 双指针
我们用哈希表 存储每个单词在数组中出现的所有下标,然后用双指针 和 分别指向两个单词在数组中出现的下标列表 和 ,每次更新下标差值的最小值,然后移动下标较小的指针,直到其中一个指针遍历完下标列表。
初始化的时间复杂度为 ,其中 为数组的长度。每次调用 shortest 方法的时间复杂度为 ,其中 为两个单词在数组中出现的下标列表的长度之和。
class WordDistance { private Map<String, List<Integer>> d = new HashMap<>(); public WordDistance(String[] wordsDict) { for (int i = 0; i < wordsDict.length; ++i) { d.computeIfAbsent(wordsDict[i], k -> new ArrayList<>()).add(i); } } public int shortest(String word1, String word2) { List<Integer> a = d.get(word1), b = d.get(word2); int ans = 0x3f3f3f3f; int i = 0, j = 0; while (i < a.size() && j < b.size()) { ans = Math.min(ans, Math.abs(a.get(i) - b.get(j))); if (a.get(i) <= b.get(j)) { ++i; } else { ++j; } } return ans; } } /** * Your WordDistance object will be instantiated and called as such: * WordDistance obj = new WordDistance(wordsDict); * int param_1 = obj.shortest(word1,word2); */
给定一个字符串数组 wordsDict 和两个字符串 word1 和 word2 ,返回这两个单词在列表中出现的最短距离。
注意:word1 和 word2 是有可能相同的,并且它们将分别表示为列表中 两个独立的单词 。
示例 1:
输入:wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "coding" 输出:1
示例 2:
输入:wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "makes" 输出:3
提示:
1 <= wordsDict.length <= 1051 <= wordsDict[i].length <= 10wordsDict[i] 由小写英文字母组成word1 和 word2 都在 wordsDict 中方法一:分情况讨论
先判断 word1 和 word2 是否相等:
如果相等,遍历数组 wordsDict,找到两个 word1 的下标 和 ,求 的最小值。
如果不相等,遍历数组 wordsDict,找到 word1 和 word2 的下标 和 ,求 的最小值。
时间复杂度 ,空间复杂度 。其中 为数组 wordsDict 的长度。
class Solution { public int shortestWordDistance(String[] wordsDict, String word1, String word2) { int ans = wordsDict.length; if (word1.equals(word2)) { for (int i = 0, j = -1; i < wordsDict.length; ++i) { if (wordsDict[i].equals(word1)) { if (j != -1) { ans = Math.min(ans, i - j); } j = i; } } } else { for (int k = 0, i = -1, j = -1; k < wordsDict.length; ++k) { if (wordsDict[k].equals(word1)) { i = k; } if (wordsDict[k].equals(word2)) { j = k; } if (i != -1 && j != -1) { ans = Math.min(ans, Math.abs(i - j)); } } } return ans; } }
中心对称数是指一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。
请写一个函数来判断该数字是否是中心对称数,其输入将会以一个字符串的形式来表达数字。
示例 1:
输入: num = "69" 输出: true
示例 2:
输入: num = "88" 输出: true
示例 3:
输入: num = "962" 输出: false
示例 4:
输入:num = "1" 输出:true
方法一:双指针模拟
我们定义一个数组 ,其中 表示数字 旋转 180° 之后的数字。如果 为 ,表示数字 不能旋转 180° 得到一个数字。
定义两个指针 和 ,分别指向字符串的左右两端,然后不断向中间移动指针,判断 和 是否相等,如果不相等,说明该字符串不是中心对称数,直接返回 即可。如果 ,说明遍历完了字符串,返回 。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
class Solution { public boolean isStrobogrammatic(String num) { int[] d = new int[] {0, 1, -1, -1, -1, -1, 9, -1, 8, 6}; for (int i = 0, j = num.length() - 1; i <= j; ++i, --j) { int a = num.charAt(i) - '0', b = num.charAt(j) - '0'; if (d[a] != b) { return false; } } return true; } }
给定一个整数 n ,返回所有长度为 n 的 中心对称数 。你可以以 任何顺序 返回答案。
中心对称数 是一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。
示例 1:
输入:n = 2 输出:["11","69","88","96"]
示例 2:
输入:n = 1 输出:["0","1","8"]
提示:
1 <= n <= 14方法一:递归
若长度为 ,则中心对称数只有 ;若长度为 ,则中心对称数只有 。
我们设计递归函数 ,其返回长度为 的中心对称数。答案为 。
若 为 ,返回包含一个空串的列表,即 [""];若 为 ,返回列表 ["0", "1", "8"]。
若 大于 ,我们对长度为 的所有中心对称数进行遍历,对于每个中心对称数 ,在其左右两侧分别添加 ,即可得到长度为 u 的中心对称数。
注意,如果 ,我们还可以在中心对称数的左右两侧分别添加 。
最终,我们将所有长度为 的中心对称数返回即可。
时间复杂度为 。
相似题目:248. 中心对称数 III
class Solution { private static final int[][] PAIRS = {{1, 1}, {8, 8}, {6, 9}, {9, 6}}; private int n; public List<String> findStrobogrammatic(int n) { this.n = n; return dfs(n); } private List<String> dfs(int u) { if (u == 0) { return Collections.singletonList(""); } if (u == 1) { return Arrays.asList("0", "1", "8"); } List<String> ans = new ArrayList<>(); for (String v : dfs(u - 2)) { for (var p : PAIRS) { ans.add(p[0] + v + p[1]); } if (u != n) { ans.add(0 + v + 0); } } return ans; } }
给定两个字符串 low 和 high 表示两个整数 low 和 high ,其中 low <= high ,返回 范围 [low, high] 内的 「中心对称数」总数 。
中心对称数 是一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。
示例 1:
输入: low = "50", high = "100" 输出: 3
示例 2:
输入: low = "0", high = "0" 输出: 1
提示:
1 <= low.length, high.length <= 15low 和 high 只包含数字low <= highlow and high 不包含任何前导零,除了零本身。方法一:递归
若长度为 ,则中心对称数只有 ;若长度为 ,则中心对称数只有 。
我们设计递归函数 ,其返回长度为 的中心对称数。
若 为 ,返回包含一个空串的列表,即 [""];若 为 ,返回列表 ["0", "1", "8"]。
若 大于 ,我们对长度为 的所有中心对称数进行遍历,对于每个中心对称数 ,在其左右两侧分别添加 ,即可得到长度为 的中心对称数。
注意,如果 ,我们还可以在中心对称数的左右两侧分别添加 。
设 和 的长度分别为 和 。
接下来,我们在 范围内遍历所有长度,对于每个长度 ,我们获取所有中心对称数 ,然后判断是否在 范围内,若在,答案加一。
时间复杂度为 。
相似题目:247. 中心对称数 II
class Solution { private static final int[][] PAIRS = {{1, 1}, {8, 8}, {6, 9}, {9, 6}}; private int n; public int strobogrammaticInRange(String low, String high) { int a = low.length(), b = high.length(); long l = Long.parseLong(low), r = Long.parseLong(high); int ans = 0; for (n = a; n <= b; ++n) { for (String s : dfs(n)) { long v = Long.parseLong(s); if (l <= v && v <= r) { ++ans; } } } return ans; } private List<String> dfs(int u) { if (u == 0) { return Collections.singletonList(""); } if (u == 1) { return Arrays.asList("0", "1", "8"); } List<String> ans = new ArrayList<>(); for (String v : dfs(u - 2)) { for (var p : PAIRS) { ans.add(p[0] + v + p[1]); } if (u != n) { ans.add(0 + v + 0); } } return ans; } }
给定一个字符串,对该字符串可以进行 “移位” 的操作,也就是将字符串中每个字母都变为其在字母表中后续的字母,比如:"abc" -> "bcd"。这样,我们可以持续进行 “移位” 操作,从而生成如下移位序列:
"abc" -> "bcd" -> ... -> "xyz"
给定一个包含仅小写字母字符串的列表,将该列表中所有满足 “移位” 操作规律的组合进行分组并返回。
示例:
输入:["abc", "bcd", "acef", "xyz", "az", "ba", "a", "z"] 输出: [ ["abc","bcd","xyz"], ["az","ba"], ["acef"], ["a","z"] ] 解释:可以认为字母表首尾相接,所以 'z' 的后续为 'a',所以 ["az","ba"] 也满足 “移位” 操作规律。
将每个字符串第一个字母变成 'a'。
class Solution { public List<List<String>> groupStrings(String[] strings) { Map<String, List<String>> mp = new HashMap<>(); for (String s : strings) { int diff = s.charAt(0) - 'a'; char[] t = s.toCharArray(); for (int i = 0; i < t.length; ++i) { char d = (char) (t[i] - diff); if (d < 'a') { d += 26; } t[i] = d; } String key = new String(t); mp.computeIfAbsent(key, k -> new ArrayList<>()).add(s); } return new ArrayList<>(mp.values()); } }
给定一个二叉树,统计该二叉树数值相同的子树个数。
同值子树是指该子树的所有节点都拥有相同的数值。
示例:
输入: root = [5,1,5,5,5,null,5]
5
/ \
1 5
/ \ \
5 5 5
输出: 4
方法一:递归
我们设计一个递归函数 ,该函数返回以 为根的子树中所有节点的值是否相同。
函数 的递归过程如下:
true;false 或者 为 false,则返回 false;如果 的左子树不为空且 的左子树的值不等于 的值,或者 的右子树不为空且 的右子树的值不等于 的值,则返回 false;否则,我们将答案加一,并返回 true。递归结束后,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { private int ans; public int countUnivalSubtrees(TreeNode root) { dfs(root); return ans; } private boolean dfs(TreeNode root) { if (root == null) { return true; } boolean l = dfs(root.left); boolean r = dfs(root.right); if (!l || !r) { return false; } int a = root.left == null ? root.val : root.left.val; int b = root.right == null ? root.val : root.right.val; if (a == b && b == root.val) { ++ans; return true; } return false; } }
请设计并实现一个能够展开二维向量的迭代器。该迭代器需要支持 next 和 hasNext 两种操作。
示例:
Vector2D iterator = new Vector2D([[1,2],[3],[4]]); iterator.next(); // 返回 1 iterator.next(); // 返回 2 iterator.next(); // 返回 3 iterator.hasNext(); // 返回 true iterator.hasNext(); // 返回 true iterator.next(); // 返回 4 iterator.hasNext(); // 返回 false
注意:
next() 的调用总是合法的,即当 next() 被调用时,二维向量总是存在至少一个后续元素。进阶:尝试在代码中仅使用 C++ 提供的迭代器 或 Java 提供的迭代器。
方法一:双指针
我们定义两个指针 和 ,分别指向当前二维向量的行和列,初始时 ,。
接下来,我们设计一个函数 ,用于将 和 向后移动,直到指向一个非空的元素。
每次调用 next 方法时,我们先调用 ,然后返回当前指向的元素,最后将 向后移动一位。
每次调用 hasNext 方法时,我们先调用 ,然后判断 是否小于二维向量的行数,如果是,则返回 true,否则返回 false。
时间复杂度 ,空间复杂度 。
class Vector2D { private int i; private int j; private int[][] vec; public Vector2D(int[][] vec) { this.vec = vec; } public int next() { forward(); return vec[i][j++]; } public boolean hasNext() { forward(); return i < vec.length; } private void forward() { while (i < vec.length && j >= vec[i].length) { ++i; j = 0; } } } /** * Your Vector2D object will be instantiated and called as such: * Vector2D obj = new Vector2D(vec); * int param_1 = obj.next(); * boolean param_2 = obj.hasNext(); */
给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议。
示例 1:
输入:intervals = [[0,30],[5,10],[15,20]] 输出:false
示例 2:
输入:intervals = [[7,10],[2,4]] 输出:true
提示:
0 <= intervals.length <= 104intervals[i].length == 20 <= starti < endi <= 106方法一:排序
我们将会议按照开始时间进行排序,然后遍历排序后的会议,如果当前会议的开始时间小于前一个会议的结束时间,则说明两个会议有重叠,返回 false 即可。
遍历结束后,返回 true。
时间复杂度 ,空间复杂度 。其中 为会议数量。
class Solution { public boolean canAttendMeetings(int[][] intervals) { Arrays.sort(intervals, (a, b) -> a[0] - b[0]); for (int i = 1; i < intervals.length; ++i) { var a = intervals[i - 1]; var b = intervals[i]; if (a[1] > b[0]) { return false; } } return true; } }
给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,返回 所需会议室的最小数量 。
示例 1:
输入:intervals = [[0,30],[5,10],[15,20]] 输出:2
示例 2:
输入:intervals = [[7,10],[2,4]] 输出:1
提示:
1 <= intervals.length <= 1040 <= starti < endi <= 106方法一:差分数组
class Solution { public int minMeetingRooms(int[][] intervals) { int n = 1000010; int[] delta = new int[n]; for (int[] e : intervals) { ++delta[e[0]]; --delta[e[1]]; } int res = delta[0]; for (int i = 1; i < n; ++i) { delta[i] += delta[i - 1]; res = Math.max(res, delta[i]); } return res; } }
整数可以被看作是其因子的乘积。
例如:
8 = 2 x 2 x 2; = 2 x 4.
请实现一个函数,该函数接收一个整数 n 并返回该整数所有的因子组合。
注意:
示例 1:
输入: 1 输出: []
示例 2:
输入: 37 输出: []
示例 3:
输入: 12 输出: [ [2, 6], [2, 2, 3], [3, 4] ]
示例 4:
输入: 32 输出: [ [2, 16], [2, 2, 8], [2, 2, 2, 4], [2, 2, 2, 2, 2], [2, 4, 4], [4, 8] ]
方法一:回溯
我们设计函数 ,其中 表示当前待分解的数, 表示当前分解的数的最大因子,函数的作用是将 分解为若干个因子,其中每个因子都不小于 ,并将所有分解结果保存到 中。
在函数 中,我们从 开始枚举 的因子 ,如果 是 的因子,那么我们将 加入当前分解结果,然后继续分解 ,即调用函数 。
时间复杂度 。
class Solution { private List<Integer> t = new ArrayList<>(); private List<List<Integer>> ans = new ArrayList<>(); public List<List<Integer>> getFactors(int n) { dfs(n, 2); return ans; } private void dfs(int n, int i) { if (!t.isEmpty()) { List<Integer> cp = new ArrayList<>(t); cp.add(n); ans.add(cp); } for (int j = i; j <= n / j; ++j) { if (n % j == 0) { t.add(j); dfs(n / j, j); t.remove(t.size() - 1); } } } }
给定一个 无重复元素 的整数数组 preorder , 如果它是以二叉搜索树的先序遍历排列 ,返回 true 。
示例 1:

输入: preorder = [5,2,1,3,6] 输出: true
示例 2:
输入: preorder = [5,2,6,1,3] 输出: false
提示:
1 <= preorder.length <= 1041 <= preorder[i] <= 104preorder 中 无重复元素进阶:您能否使用恒定的空间复杂度来完成此题?
二叉搜索树先序遍历时,每次移向左子树时,值递减,移向右子树时,值递增。
因此,可以维护一个单调递减栈。遍历序列,若当前值大于栈顶元素,说明开始要进入右子树的遍历。只要栈顶元素比当前值小,就表示还是左子树,要移除,也就是从栈中弹出,直至栈顶元素大于当前值,或者栈为空。此过程要记录弹出栈的最后一个元素 last。
接下来继续往后遍历,之后右子树的每个节点,都要比 last 大,才能满足二叉搜索树,否则直接返回 false。
class Solution { public boolean verifyPreorder(int[] preorder) { Deque<Integer> stk = new ArrayDeque<>(); int last = Integer.MIN_VALUE; for (int x : preorder) { if (x < last) { return false; } while (!stk.isEmpty() && stk.peek() < x) { last = stk.poll(); } stk.push(x); } return true; } }
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
示例 1:
输入: costs = [[17,2,17],[16,16,5],[14,3,19]] 输出: 10 解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。 最少花费: 2 + 5 + 3 = 10。
示例 2:
输入: costs = [[7,6,2]] 输出: 2
提示:
costs.length == ncosts[i].length == 31 <= n <= 1001 <= costs[i][j] <= 20方法一:动态规划
时间复杂度 ,空间复杂度 。其中 表示房子的数量。
class Solution { public int minCost(int[][] costs) { int r = 0, g = 0, b = 0; for (int[] cost : costs) { int _r = r, _g = g, _b = b; r = Math.min(_g, _b) + cost[0]; g = Math.min(_r, _b) + cost[1]; b = Math.min(_r, _g) + cost[2]; } return Math.min(r, Math.min(g, b)); } }
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5] 输出:["1->2->5","1->3"]
示例 2:
输入:root = [1] 输出:["1"]
提示:
[1, 100] 内-100 <= Node.val <= 100深度优先搜索+路径记录。
class Solution { private List<String> ans; private List<String> t; public List<String> binaryTreePaths(TreeNode root) { ans = new ArrayList<>(); t = new ArrayList<>(); dfs(root); return ans; } private void dfs(TreeNode root) { if (root == null) { return; } t.add(root.val + ""); if (root.left == null && root.right == null) { ans.add(String.join("->", t)); } dfs(root.left); dfs(root.right); t.remove(t.size() - 1); } }
给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
示例 1:
输入: num = 38 输出: 2 解释: 各位相加的过程为: 38 --> 3 + 8 --> 11 11 --> 1 + 1 --> 2 由于 2 是一位数,所以返回 2。
示例 2:
输入: num = 0 输出: 0
提示:
0 <= num <= 231 - 1进阶:你可以不使用循环或者递归,在 O(1) 时间复杂度内解决这个问题吗?
题目要求的数叫做“数根”,我们把 1~30 的数根列出来:
原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3
可以看到,数根 9 个为一组,循环出现。我们可以得出下面的规律:
将上面的规律用式子:(n - 1) % 9 + 1 统一表达。
class Solution { public int addDigits(int num) { return (num - 1) % 9 + 1; } }
给定一个长度为 n 的整数数组和一个目标值 target ,寻找能够使条件 nums[i] + nums[j] + nums[k] < target 成立的三元组 i, j, k 个数(0 <= i < j < k < n)。
示例 1:
输入: nums = [-2,0,1,3], target = 2
输出: 2
解释: 因为一共有两个三元组满足累加和小于 2:
[-2,0,1]
[-2,0,3]
示例 2:
输入: nums = [], target = 0 输出: 0
示例 3:
输入: nums = [0], target = 0 输出: 0
提示:
n == nums.length0 <= n <= 3500-100 <= nums[i] <= 100-100 <= target <= 100双指针解决。
class Solution { public int threeSumSmaller(int[] nums, int target) { Arrays.sort(nums); int ans = 0; for (int i = 0, n = nums.length; i < n; ++i) { int j = i + 1; int k = n - 1; while (j < k) { int s = nums[i] + nums[j] + nums[k]; if (s >= target) { --k; } else { ans += k - j; ++j; } } } return ans; } }
给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。
示例 1:
输入:nums = [1,2,1,3,2,5] 输出:[3,5] 解释:[5, 3] 也是有效的答案。
示例 2:
输入:nums = [-1,0] 输出:[-1,0]
示例 3:
输入:nums = [0,1] 输出:[1,0]
提示:
2 <= nums.length <= 3 * 104-231 <= nums[i] <= 231 - 1nums 中的其他数字都出现两次class Solution { public int[] singleNumber(int[] nums) { int eor = 0; for (int x : nums) { eor ^= x; } int lowbit = eor & (-eor); int[] ans = new int[2]; for (int x : nums) { if ((x & lowbit) == 0) { ans[0] ^= x; } } ans[1] = eor ^ ans[0]; return ans; } }
给定编号从 0 到 n - 1 的 n 个结点。给定一个整数 n 和一个 edges 列表,其中 edges[i] = [ai, bi] 表示图中节点 ai 和 bi 之间存在一条无向边。
如果这些边能够形成一个合法有效的树结构,则返回 true ,否则返回 false 。
示例 1:

输入: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]] 输出: true
示例 2:

输入: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]] 输出: false
提示:
1 <= n <= 20000 <= edges.length <= 5000edges[i].length == 20 <= ai, bi < nai != bi并查集模板题。
模板 1——朴素并查集:
模板 2——维护 size 的并查集:
模板 3——维护到祖宗节点距离的并查集:
class Solution { private int[] p; public boolean validTree(int n, int[][] edges) { p = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; } for (int[] e : edges) { int a = e[0], b = e[1]; if (find(a) == find(b)) { return false; } p[find(a)] = find(b); --n; } return n == 1; } private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; } }
表:Trips
+-------------+----------+ | Column Name | Type | +-------------+----------+ | id | int | | client_id | int | | driver_id | int | | city_id | int | | status | enum | | request_at | date | +-------------+----------+ id 是这张表的主键。 这张表中存所有出租车的行程信息。每段行程有唯一 id ,其中 client_id 和 driver_id 是 Users 表中 users_id 的外键。 status 是一个表示行程状态的枚举类型,枚举成员为(‘completed’, ‘cancelled_by_driver’, ‘cancelled_by_client’) 。
表:Users
+-------------+----------+ | Column Name | Type | +-------------+----------+ | users_id | int | | banned | enum | | role | enum | +-------------+----------+ users_id 是这张表的主键。 这张表中存所有用户,每个用户都有一个唯一的 users_id ,role 是一个表示用户身份的枚举类型,枚举成员为 (‘client’, ‘driver’, ‘partner’) 。 banned 是一个表示用户是否被禁止的枚举类型,枚举成员为 (‘Yes’, ‘No’) 。
取消率 的计算方式如下:(被司机或乘客取消的非禁止用户生成的订单数量) / (非禁止用户生成的订单总数)。
写一段 SQL 语句查出 "2013-10-01" 至 "2013-10-03" 期间非禁止用户(乘客和司机都必须未被禁止)的取消率。非禁止用户即 banned 为 No 的用户,禁止用户即 banned 为 Yes 的用户。
返回结果表中的数据可以按任意顺序组织。其中取消率 Cancellation Rate 需要四舍五入保留 两位小数 。
查询结果格式如下例所示。
示例:
输入: Trips 表: +----+-----------+-----------+---------+---------------------+------------+ | id | client_id | driver_id | city_id | status | request_at | +----+-----------+-----------+---------+---------------------+------------+ | 1 | 1 | 10 | 1 | completed | 2013-10-01 | | 2 | 2 | 11 | 1 | cancelled_by_driver | 2013-10-01 | | 3 | 3 | 12 | 6 | completed | 2013-10-01 | | 4 | 4 | 13 | 6 | cancelled_by_client | 2013-10-01 | | 5 | 1 | 10 | 1 | completed | 2013-10-02 | | 6 | 2 | 11 | 6 | completed | 2013-10-02 | | 7 | 3 | 12 | 6 | completed | 2013-10-02 | | 8 | 2 | 12 | 12 | completed | 2013-10-03 | | 9 | 3 | 10 | 12 | completed | 2013-10-03 | | 10 | 4 | 13 | 12 | cancelled_by_driver | 2013-10-03 | +----+-----------+-----------+---------+---------------------+------------+ Users 表: +----------+--------+--------+ | users_id | banned | role | +----------+--------+--------+ | 1 | No | client | | 2 | Yes | client | | 3 | No | client | | 4 | No | client | | 10 | No | driver | | 11 | No | driver | | 12 | No | driver | | 13 | No | driver | +----------+--------+--------+ 输出: +------------+-------------------+ | Day | Cancellation Rate | +------------+-------------------+ | 2013-10-01 | 0.33 | | 2013-10-02 | 0.00 | | 2013-10-03 | 0.50 | +------------+-------------------+ 解释: 2013-10-01: - 共有 4 条请求,其中 2 条取消。 - 然而,id=2 的请求是由禁止用户(user_id=2)发出的,所以计算时应当忽略它。 - 因此,总共有 3 条非禁止请求参与计算,其中 1 条取消。 - 取消率为 (1 / 3) = 0.33 2013-10-02: - 共有 3 条请求,其中 0 条取消。 - 然而,id=6 的请求是由禁止用户发出的,所以计算时应当忽略它。 - 因此,总共有 2 条非禁止请求参与计算,其中 0 条取消。 - 取消率为 (0 / 2) = 0.00 2013-10-03: - 共有 3 条请求,其中 1 条取消。 - 然而,id=8 的请求是由禁止用户发出的,所以计算时应当忽略它。 - 因此,总共有 2 条非禁止请求参与计算,其中 1 条取消。 - 取消率为 (1 / 2) = 0.50
丑数 就是只包含质因数 2、3 和 5 的正整数。
给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:n = 6 输出:true 解释:6 = 2 × 3
示例 2:
输入:n = 1
输出:true
解释:1 没有质因数,因此它的全部质因数是 {2, 3, 5} 的空集。习惯上将其视作第一个丑数。
示例 3:
输入:n = 14 输出:false 解释:14 不是丑数,因为它包含了另外一个质因数 7 。
提示:
-231 <= n <= 231 - 1n < 1,说明 n 一定不是丑数,返回 false。n % 2 == 0,说明 2 是 n 的因子,此时应 n /= 2,然后继续判断 n 除以 2 后的值的因子。n % 3 == 0,说明 3 是 n 的因子,此时应 n /= 3,然后继续判断 n 除以 3 后的值的因子。n % 5 == 0,说明 5 是 n 的因子,此时应 n /= 5,然后继续判断 n 除以 5 后的值的因子。class Solution { public boolean isUgly(int n) { if (n < 1) return false; while (n % 2 == 0) { n /= 2; } while (n % 3 == 0) { n /= 3; } while (n % 5 == 0) { n /= 5; } return n == 1; } }
给你一个整数 n ,请你找出并返回第 n 个 丑数 。
丑数 就是只包含质因数 2、3 和/或 5 的正整数。
示例 1:
输入:n = 10 输出:12 解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
示例 2:
输入:n = 1 输出:1 解释:1 通常被视为丑数。
提示:
1 <= n <= 1690方法一:优先队列(最小堆)
初始时,将第一个丑数 加入堆。每次取出堆顶元素 ,由于 , , 也是丑数,因此将它们加入堆中。为了避免重复元素,可以用哈希表 去重。
时间复杂度 ,空间复杂度 。
方法二:动态规划
定义数组 ,其中 表示第 个丑数,那么第 个丑数就是 。最小的丑数是 ,所以 。
定义 个指针 , 和 ,表示下一个丑数是当前指针指向的丑数乘以对应的质因数。初始时,三个指针的值都指向 。
当 在 范围内,我们更新 ,然后分别比较 与 , , 是否相等,若是,则对应的指针加 。
最后返回 即可。
时间复杂度 ,空间复杂度 。
class Solution { public int nthUglyNumber(int n) { Set<Long> vis = new HashSet<>(); PriorityQueue<Long> q = new PriorityQueue<>(); int[] f = new int[]{2, 3, 5}; q.offer(1L); vis.add(1L); long ans = 0; while (n-- > 0) { ans = q.poll(); for (int v : f) { long next = ans * v; if (vis.add(next)) { q.offer(next); } } } return (int) ans; } }
class Solution { public int nthUglyNumber(int n) { int[] dp = new int[n]; dp[0] = 1; int p2 = 0, p3 = 0, p5 = 0; for (int i = 1; i < n; ++i) { int next2 = dp[p2] * 2, next3 = dp[p3] * 3, next5 = dp[p5] * 5; dp[i] = Math.min(next2, Math.min(next3, next5)); if (dp[i] == next2) ++p2; if (dp[i] == next3) ++p3; if (dp[i] == next5) ++p5; } return dp[n - 1]; } }
假如有一排房子共有 n 幢,每个房子可以被粉刷成 k 种颜色中的一种。房子粉刷成不同颜色的花费成本也是不同的。你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
每个房子粉刷成不同颜色的花费以一个 n x k 的矩阵表示。
costs[0][0] 表示第 0 幢房子粉刷成 0 号颜色的成本;costs[1][2] 表示第 1 幢房子粉刷成 2 号颜色的成本,以此类推。返回 粉刷完所有房子的最低成本 。
示例 1:
输入: costs = [[1,5,3],[2,9,4]] 输出: 5 解释: 将房子 0 刷成 0 号颜色,房子 1 刷成 2 号颜色。花费: 1 + 4 = 5; 或者将 房子 0 刷成 2 号颜色,房子 1 刷成 0 号颜色。花费: 3 + 2 = 5.
示例 2:
输入: costs = [[1,3],[2,4]] 输出: 5
提示:
costs.length == ncosts[i].length == k1 <= n <= 1002 <= k <= 201 <= costs[i][j] <= 20进阶:您能否在 O(nk) 的时间复杂度下解决此问题?
方法一:动态规划
定义 表示粉刷前 个房子,且最后一个房子被粉刷成第 种颜色的最小花费。答案为 。
对于 ,可以从 转移而来,其中 。因此,可以得到状态转移方程:
由于 只与 有关,因此可以使用滚动数组优化空间复杂度。
时间复杂度 ,空间复杂度 。其中 和 分别为房子数量和颜色数量。
class Solution { public int minCostII(int[][] costs) { int n = costs.length, k = costs[0].length; int[] f = costs[0].clone(); for (int i = 1; i < n; ++i) { int[] g = costs[i].clone(); for (int j = 0; j < k; ++j) { int t = Integer.MAX_VALUE; for (int h = 0; h < k; ++h) { if (h != j) { t = Math.min(t, f[h]); } } g[j] += t; } f = g; } return Arrays.stream(f).min().getAsInt(); } }
给定一个字符串,判断该字符串中是否可以通过重新排列组合,形成一个回文字符串。
示例 1:
输入: "code" 输出: false
示例 2:
输入: "aab" 输出: true
示例 3:
输入: "carerac" 输出: true
方法一:数组
创建一个长度为 的数组,统计每个字母出现的频率,至多有一个字符出现奇数次数即可。
时间复杂度 ,空间复杂度 。其中 是字符串的长度。
方法二:哈希表
利用哈希表来维护元素。遍历字符串每个字母 ,若 在哈希表中,则将 从哈希表中删除,否则将 加入哈希表。
遍历结束,若哈希表中元素个数不超过 ,则返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 是字符串的长度。
class Solution { public boolean canPermutePalindrome(String s) { int[] cnt = new int[26]; for (char c : s.toCharArray()) { ++cnt[c - 'a']; } int n = 0; for (int v : cnt) { n += v % 2; } return n < 2; } }
给定一个字符串 s ,返回 其重新排列组合后可能构成的所有回文字符串,并去除重复的组合 。
你可以按 任意顺序 返回答案。如果 s 不能形成任何回文排列时,则返回一个空列表。
示例 1:
输入: s = "aabb" 输出: ["abba", "baab"]
示例 2:
输入: s = "abc" 输出: []
提示:
1 <= s.length <= 16s 仅由小写英文字母组成方法一:回溯
回文排列需要满足至多有一个字符出现奇数次数。若不满足条件,答案提前返回。
找到出现奇数次的字符,作为中间字符(可以为空),分别向两边扩展,构造回文串。若串的长度与原串长度相等,将该串添加到答案中。
时间复杂度 。其中 为字符串 的长度。
class Solution { private List<String> ans = new ArrayList<>(); private int[] cnt = new int[26]; private int n; public List<String> generatePalindromes(String s) { n = s.length(); for (char c : s.toCharArray()) { ++cnt[c - 'a']; } String mid = ""; for (int i = 0; i < 26; ++i) { if (cnt[i] % 2 == 1) { if (!"".equals(mid)) { return ans; } mid = String.valueOf((char) (i + 'a')); } } dfs(mid); return ans; } private void dfs(String t) { if (t.length() == n) { ans.add(t); return; } for (int i = 0; i < 26; ++i) { if (cnt[i] > 1) { String c = String.valueOf((char) (i + 'a')); cnt[i] -= 2; dfs(c + t + c); cnt[i] += 2; } } } }
给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。
示例 1:
输入:nums = [3,0,1] 输出:2 解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:
输入:nums = [0,1] 输出:2 解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1] 输出:8 解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
示例 4:
输入:nums = [0] 输出:1 解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。
提示:
n == nums.length1 <= n <= 1040 <= nums[i] <= nnums 中的所有数字都 独一无二进阶:你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?
方法一:位运算
对于数组中的每个元素,都可以与下标进行异或运算,最终的结果就是缺失的数字。
时间复杂度 ,空间复杂度 。其中 为数组长度。
方法二:数学
我们也可以用数学求解。求出 的和,减去数组中所有数的和,就得到了缺失的数字。
时间复杂度 ,空间复杂度 。其中 为数组长度。
class Solution { public int missingNumber(int[] nums) { int n = nums.length; int ans = n; for (int i = 0; i < n; ++i) { ans ^= (i ^ nums[i]); } return ans; } }
class Solution { public int missingNumber(int[] nums) { int n = nums.length; int ans = n; for (int i = 0; i < n; ++i) { ans += i - nums[i]; } return ans; } }
现有一种使用英语字母的火星语言,这门语言的字母顺序与英语顺序不同。
给你一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。
字符串 s 字典顺序小于 字符串 t 有两种情况:
s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。示例 1:
输入:words = ["wrt","wrf","er","ett","rftt"] 输出:"wertf"
示例 2:
输入:words = ["z","x"] 输出:"zx"
示例 3:
输入:words = ["z","x","z"] 输出:"" 解释:不存在合法字母顺序,因此返回 "" 。
提示:
1 <= words.length <= 1001 <= words[i].length <= 100words[i] 仅由小写英文字母组成方法一:拓扑排序 + BFS
用数组 记录在火星字典中的字母先后关系, 表示字母 在字母 的前面;用数组 记录当前字典出现过的字母, 表示出现过的字母数。
一个很简单的想法是遍历每一个单词,比较该单词和其后的所有单词,把所有的先后关系更新进数组 ,这样遍历时间复杂度为 ;但是我们发现其实比较相邻的两个单词就可以了,比如 则比较 和 , 和 的关系便确定了。因此算法可以优化成比较相邻两个单词,时间复杂度为 。
出现矛盾的情况:
拓扑排序:
class Solution { public String alienOrder(String[] words) { boolean[][] g = new boolean[26][26]; boolean[] s = new boolean[26]; int cnt = 0; int n = words.length; for (int i = 0; i < n - 1; ++i) { for (char c : words[i].toCharArray()) { if (cnt == 26) { break; } c -= 'a'; if (!s[c]) { ++cnt; s[c] = true; } } int m = words[i].length(); for (int j = 0; j < m; ++j) { if (j >= words[i + 1].length()) { return ""; } char c1 = words[i].charAt(j), c2 = words[i + 1].charAt(j); if (c1 == c2) { continue; } if (g[c2 - 'a'][c1 - 'a']) { return ""; } g[c1 - 'a'][c2 - 'a'] = true; break; } } for (char c : words[n - 1].toCharArray()) { if (cnt == 26) { break; } c -= 'a'; if (!s[c]) { ++cnt; s[c] = true; } } int[] indegree = new int[26]; for (int i = 0; i < 26; ++i) { for (int j = 0; j < 26; ++j) { if (i != j && s[i] && s[j] && g[i][j]) { ++indegree[j]; } } } Deque<Integer> q = new LinkedList<>(); for (int i = 0; i < 26; ++i) { if (s[i] && indegree[i] == 0) { q.offerLast(i); } } StringBuilder ans = new StringBuilder(); while (!q.isEmpty()) { int t = q.pollFirst(); ans.append((char) (t + 'a')); for (int i = 0; i < 26; ++i) { if (i != t && s[i] && g[t][i]) { if (--indegree[i] == 0) { q.offerLast(i); } } } } return ans.length() < cnt ? "" : ans.toString(); } }
给你二叉搜索树的根节点 root 和一个目标值 target ,请在该二叉搜索树中找到最接近目标值 target 的数值。如果有多个答案,返回最小的那个。
示例 1:
输入:root = [4,2,5,1,3], target = 3.714286 输出:4
示例 2:
输入:root = [1], target = 4.428571 输出:1
提示:
[1, 104] 内0 <= Node.val <= 109-109 <= target <= 109二分查找。
class Solution { public int closestValue(TreeNode root, double target) { int ans = root.val; double mi = Double.MAX_VALUE; while (root != null) { double t = Math.abs(root.val - target); if (t < mi) { mi = t; ans = root.val; } if (root.val > target) { root = root.left; } else { root = root.right; } } return ans; } }
请你设计一个算法,可以将一个 字符串列表 编码成为一个 字符串。这个编码后的字符串是可以通过网络进行高效传送的,并且可以在接收端被解码回原来的字符串列表。
1 号机(发送方)有如下函数:
string encode(vector<string> strs) {
// ... your code
return encoded_string;
}
2 号机(接收方)有如下函数:
vector<string> decode(string s) {
//... your code
return strs;
}
1 号机(发送方)执行:
string encoded_string = encode(strs);
2 号机(接收方)执行:
vector<string> strs2 = decode(encoded_string);
此时,2 号机(接收方)的 strs2 需要和 1 号机(发送方)的 strs 相同。
请你来实现这个 encode 和 decode 方法。
注意:
eval 又或者是 serialize 之类的方法。本题的宗旨是需要您自己实现 “编码” 和 “解码” 算法。方法一:使用非 ASCII 码的分隔符
Python 中可以直接 chr(257) 作为字符串的分隔符,这样就可以实现字符串的编码和解码。
时间复杂度 。
方法二:编码字符串长度
编码时,将字符串的长度转成固定 位的字符串,加上字符串本身,依次拼接到结果字符串。
解码时,先取前四位字符串,得到长度,再通过长度截取后面的字符串。依次截取,最终得到字符串列表。
时间复杂度 。
public class Codec { // Encodes a list of strings to a single string. public String encode(List<String> strs) { StringBuilder ans = new StringBuilder(); for (String s : strs) { ans.append((char) s.length()).append(s); } return ans.toString(); } // Decodes a single string to a list of strings. public List<String> decode(String s) { List<String> ans = new ArrayList<>(); int i = 0, n = s.length(); while (i < n) { int size = s.charAt(i++); ans.add(s.substring(i, i + size)); i += size; } return ans; } } // Your Codec object will be instantiated and called as such: // Codec codec = new Codec(); // codec.decode(codec.encode(strs));
给定二叉搜索树的根 root 、一个目标值 target 和一个整数 k ,返回BST中最接近目标的 k 个值。你可以按 任意顺序 返回答案。
题目 保证 该二叉搜索树中只会存在一种 k 个值集合最接近 target
示例 1:

输入: root = [4,2,5,1,3],目标值 = 3.714286,且 k = 2 输出: [4,3]
示例 2:
输入: root = [1], target = 0.000000, k = 1 输出: [1]
提示:
n1 <= k <= n <= 1040 <= Node.val <= 109-109 <= target <= 109进阶:假设该二叉搜索树是平衡的,请问您是否能在小于 O(n)( n = total nodes )的时间复杂度内解决该问题呢?
中序遍历,当结果元素个数小于 k 时,直接添加。否则,拿第一个元素与当前节点 root 各自与 target 的差值的绝对值进行比较。
时间复杂度 ,空间复杂度 。
class Solution { private List<Integer> ans; private double target; private int k; public List<Integer> closestKValues(TreeNode root, double target, int k) { ans = new LinkedList<>(); this.target = target; this.k = k; dfs(root); return ans; } private void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); if (ans.size() < k) { ans.add(root.val); } else { if (Math.abs(root.val - target) >= Math.abs(ans.get(0) - target)) { return; } ans.remove(0); ans.add(root.val); } dfs(root.right); } }
将非负整数 num 转换为其对应的英文表示。
示例 1:
输入:num = 123 输出:"One Hundred Twenty Three"
示例 2:
输入:num = 12345 输出:"Twelve Thousand Three Hundred Forty Five"
示例 3:
输入:num = 1234567 输出:"One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"
提示:
0 <= num <= 231 - 1class Solution { private static Map<Integer, String> map; static { map = new HashMap<>(); map.put(1, "One"); map.put(2, "Two"); map.put(3, "Three"); map.put(4, "Four"); map.put(5, "Five"); map.put(6, "Six"); map.put(7, "Seven"); map.put(8, "Eight"); map.put(9, "Nine"); map.put(10, "Ten"); map.put(11, "Eleven"); map.put(12, "Twelve"); map.put(13, "Thirteen"); map.put(14, "Fourteen"); map.put(15, "Fifteen"); map.put(16, "Sixteen"); map.put(17, "Seventeen"); map.put(18, "Eighteen"); map.put(19, "Nineteen"); map.put(20, "Twenty"); map.put(30, "Thirty"); map.put(40, "Forty"); map.put(50, "Fifty"); map.put(60, "Sixty"); map.put(70, "Seventy"); map.put(80, "Eighty"); map.put(90, "Ninety"); map.put(100, "Hundred"); map.put(1000, "Thousand"); map.put(1000000, "Million"); map.put(1000000000, "Billion"); } public String numberToWords(int num) { if (num == 0) { return "Zero"; } StringBuilder sb = new StringBuilder(); for (int i = 1000000000; i >= 1000; i /= 1000) { if (num >= i) { sb.append(get3Digits(num / i)).append(' ').append(map.get(i)); num %= i; } } if (num > 0) { sb.append(get3Digits(num)); } return sb.substring(1); } private String get3Digits(int num) { StringBuilder sb = new StringBuilder(); if (num >= 100) { sb.append(' ').append(map.get(num / 100)).append(' ').append(map.get(100)); num %= 100; } if (num > 0) { if (num < 20 || num % 10 == 0) { sb.append(' ').append(map.get(num)); } else { sb.append(' ').append(map.get(num / 10 * 10)).append(' ').append(map.get(num % 10)); } } return sb.toString(); } }
class Solution { private String[] lt20 = {"", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}; private String[] tens = {"", "Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"}; private String[] thousands = {"Billion", "Million", "Thousand", ""}; public String numberToWords(int num) { if (num == 0) { return "Zero"; } StringBuilder sb = new StringBuilder(); for (int i = 1000000000, j = 0; i > 0; i /= 1000, ++j) { if (num / i == 0) { continue; } sb.append(transfer(num / i)).append(thousands[j]).append(' '); num %= i; } return sb.toString().trim(); } private String transfer(int num) { if (num == 0) { return ""; } if (num < 20) { return lt20[num] + " "; } if (num < 100) { return tens[num / 10] + " " + transfer(num % 10); } return lt20[num / 100] + " Hundred " + transfer(num % 100); } }
给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。
根据维基百科上 h 指数的定义:h 代表“高引用次数”,一名科研人员的 h指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。且其余的 n - h 篇论文每篇被引用次数 不超过 h 次。
如果 h 有多种可能的值,h 指数 是其中最大的那个。
示例 1:
输入:citations = [3,0,6,1,5] 输出:3 解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
示例 2:
输入:citations = [1,3,1] 输出:1
提示:
n == citations.length1 <= n <= 50000 <= citations[i] <= 1000方法一:排序
我们可以先对数组 citations 按照元素值从大到小进行排序。然后我们从大到小枚举 值,如果某个 值满足 ,则说明有至少 篇论文分别被引用了至少 次,直接返回 即可。如果没有找到这样的 值,说明所有的论文都没有被引用,返回 。
时间复杂度 ,空间复杂度 。其中 是数组 citations 的长度。
方法二:计数 + 求和
我们可以使用一个长度为 的数组 ,其中 表示引用次数为 的论文的篇数。我们遍历数组 citations,将引用次数大于 的论文都当作引用次数为 的论文,然后将每篇论文的引用次数作为下标,将 中对应的元素值加 。这样我们就统计出了每个引用次数对应的论文篇数。
接下来,我们从大到小枚举 值,将 中下标为 的元素值加到变量 中,其中 表示引用次数大于等于 的论文篇数。如果 ,说明至少有 篇论文分别被引用了至少 次,直接返回 即可。
时间复杂度 ,空间复杂度 。其中 是数组 citations 的长度。
方法三:二分查找
我们注意到,如果存在一个 值满足至少有 篇论文至少被引用 次,那么对于任意一个 ,都有至少 篇论文至少被引用 次。因此我们可以使用二分查找的方法,找到最大的 值,使得至少有 篇论文至少被引用 次。
我们定义二分查找的左边界 ,右边界 。每次我们取 ,其中 表示对 向下取整。然后我们统计数组 citations 中大于等于 的元素的个数,记为 。如果 ,说明至少有 篇论文至少被引用 次,此时我们将左边界 变为 ,否则我们将右边界 变为 。当左边界 等于右边界 时,我们找到了最大的 值,即为 或 。
时间复杂度 ,其中 是数组 citations 的长度。空间复杂度 。
class Solution { public int hIndex(int[] citations) { Arrays.sort(citations); int n = citations.length; for (int h = n; h > 0; --h) { if (citations[n - h] >= h) { return h; } } return 0; } }
class Solution { public int hIndex(int[] citations) { int n = citations.length; int[] cnt = new int[n + 1]; for (int x : citations) { ++cnt[Math.min(x, n)]; } for (int h = n, s = 0; ; --h) { s += cnt[h]; if (s >= h) { return h; } } } }
class Solution { public int hIndex(int[] citations) { int l = 0, r = citations.length; while (l < r) { int mid = (l + r + 1) >> 1; int s = 0; for (int x : citations) { if (x >= mid) { ++s; } } if (s >= mid) { l = mid; } else { r = mid - 1; } } return l; } }
给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。
h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。
请你设计并实现对数时间复杂度的算法解决此问题。
示例 1:
输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3 。
示例 2:
输入:citations = [1,2,100] 输出:2
提示:
n == citations.length1 <= n <= 1050 <= citations[i] <= 1000citations 按 升序排列方法一:二分查找
二分枚举 h,获取满足条件的最大 h。由于要满足 h 篇论文至少被引用 h 次,因此 citations[n - mid] >= mid。
时间复杂度 O(logn)。
class Solution { public int hIndex(int[] citations) { int n = citations.length; int left = 0, right = n; while (left < right) { int mid = (left + right) >>> 1; if (citations[mid] >= n - mid) { right = mid; } else { left = mid + 1; } } return n - left; } }
有 k 种颜色的涂料和一个包含 n 个栅栏柱的栅栏,请你按下述规则为栅栏设计涂色方案:
给你两个整数 k 和 n ,返回所有有效的涂色 方案数 。
示例 1:
输入:n = 3, k = 2 输出:6 解释:所有的可能涂色方案如上图所示。注意,全涂红或者全涂绿的方案属于无效方案,因为相邻的栅栏柱 最多连续两个 颜色相同。
示例 2:
输入:n = 1, k = 1 输出:1
示例 3:
输入:n = 7, k = 2 输出:42
提示:
1 <= n <= 501 <= k <= 105n 和 k ,其答案在范围 [0, 231 - 1] 内方法一:动态规划
定义 表示栅栏 且最后两个栅栏颜色不同的方案数, 表示栅栏 且最后两个栅栏颜色相同的方案数。
初始时 。当 时,有:
答案为 。
时间复杂度 ,空间复杂度 。其中 是栅栏柱的数量。
class Solution { public int numWays(int n, int k) { int[][] dp = new int[n][2]; dp[0][0] = k; for (int i = 1; i < n; ++i) { dp[i][0] = (dp[i - 1][0] + dp[i - 1][1]) * (k - 1); dp[i][1] = dp[i - 1][0]; } return dp[n - 1][0] + dp[n - 1][1]; } }
假设你是一个专业的狗仔,参加了一个 n 人派对,其中每个人被从 0 到 n - 1 标号。在这个派对人群当中可能存在一位 “名人”。所谓 “名人” 的定义是:其他所有 n - 1 个人都认识他/她,而他/她并不认识其他任何人。
现在你想要确认这个 “名人” 是谁,或者确定这里没有 “名人”。而你唯一能做的就是问诸如 “A 你好呀,请问你认不认识 B呀?” 的问题,以确定 A 是否认识 B。你需要在(渐近意义上)尽可能少的问题内来确定这位 “名人” 是谁(或者确定这里没有 “名人”)。
在本题中,你可以使用辅助函数 bool knows(a, b) 获取到 A 是否认识 B。请你来实现一个函数 int findCelebrity(n)。
派对最多只会有一个 “名人” 参加。若 “名人” 存在,请返回他/她的编号;若 “名人” 不存在,请返回 -1。
示例 1:

输入: graph = [ [1,1,0], [0,1,0], [1,1,1] ] 输出: 1 解释: 有编号分别为 0、1 和 2 的三个人。graph[i][j] = 1 代表编号为 i 的人认识编号为 j 的人,而 graph[i][j] = 0 则代表编号为 i 的人不认识编号为 j 的人。“名人” 是编号 1 的人,因为 0 和 2 均认识他/她,但 1 不认识任何人。
示例 2:

输入: graph = [ [1,0,1], [1,1,0], [0,1,1] ] 输出: -1 解释: 没有 “名人”
提示:
n == graph.lengthn == graph[i].length2 <= n <= 100graph[i][j] 是 0 或 1.graph[i][i] == 1进阶:如果允许调用 API knows 的最大次数为 3 * n ,你可以设计一个不超过最大调用次数的解决方案吗?
方法一:O(n) 遍历
经过验证,若暴力遍历,调用 次 方法,会报 TLE 错误。因此,我们需要寻找更优的解法。
要找出 个人中的名人,题目给我们的关键信息是:1. 名人不认识其他所有人;2. 其他所有人都认识名人。
那么,我们初始时假定名人 。然后在 范围内遍历 ,若 认识 ,说明 不是我们要找的名人,此时我们可以直接将 更新为 。
为什么呢?我们来举个实际的例子。
ans = 0 for i in [1,n) { if (ans knows i) { ans = i } } ans = 0 ans not knows 1 ans not knows 2 ans knows 3 ans = 3 ans not knows 4 ans not knows 5 ans not knows 6 ans = 6
这里 认识 ,说明 不是名人(即 不是名人),那么名人会是 或者 吗?不会!因为若 或者 是名人,那么 应该认识 或者 才对,与前面的例子冲突。因此,我们可以直接将 更新为 。
我们找出 之后,接下来再遍历一遍,判断 是否满足名人的条件。若不满足,返回 。
否则遍历结束,返回 。
/* The knows API is defined in the parent class Relation. boolean knows(int a, int b); */ public class Solution extends Relation { public int findCelebrity(int n) { int ans = 0; for (int i = 1; i < n; ++i) { if (knows(ans, i)) { ans = i; } } for (int i = 0; i < n; ++i) { if (ans != i) { if (knows(ans, i) || !knows(i, ans)) { return -1; } } } return ans; } }
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例 1:
输入:n = 5, bad = 4 输出:4 解释: 调用 isBadVersion(3) -> false 调用 isBadVersion(5) -> true 调用 isBadVersion(4) -> true 所以,4 是第一个错误的版本。
示例 2:
输入:n = 1, bad = 1 输出:1
提示:
1 <= bad <= n <= 231 - 1二分查找。
/* The isBadVersion API is defined in the parent class VersionControl. boolean isBadVersion(int version); */ public class Solution extends VersionControl { public int firstBadVersion(int n) { int left = 1, right = n; while (left < right) { int mid = (left + right) >>> 1; if (isBadVersion(mid)) { right = mid; } else { left = mid + 1; } } return left; } }
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9
提示:
1 <= n <= 104动态规划,定义 dp[i] 表示和为 i 的完全平方数的最少数量。
class Solution { public int numSquares(int n) { int[] dp = new int[n + 1]; for (int i = 1; i <= n; ++i) { int mi = Integer.MAX_VALUE; for (int j = 1; j * j <= i; ++j) { mi = Math.min(mi, dp[i - j * j]); } dp[i] = mi + 1; } return dp[n]; } }
给你一个的整数数组 nums, 将该数组重新排序后使 nums[0] <= nums[1] >= nums[2] <= nums[3]...
输入数组总是有一个有效的答案。
示例 1:
输入:nums = [3,5,2,1,6,4] 输出:[3,5,1,6,2,4] 解释:[1,6,2,5,3,4]也是有效的答案
示例 2:
输入:nums = [6,6,5,6,3,8] 输出:[6,6,5,6,3,8]
提示:
1 <= nums.length <= 5 * 1040 <= nums[i] <= 104输入的 nums 保证至少有一个答案。
进阶:你能在 O(n) 时间复杂度下解决这个问题吗?
class Solution { public void wiggleSort(int[] nums) { for (int i = 1; i < nums.length; ++i) { if ((i % 2 == 1 && nums[i] < nums[i - 1]) || (i % 2 == 0 && nums[i] > nums[i - 1])) { swap(nums, i, i - 1); } } } private void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } }
给出两个一维的向量,请你实现一个迭代器,交替返回它们中间的元素。
示例:
输入: v1 = [1,2] v2 = [3,4,5,6] 输出: [1,3,2,4,5,6] 解析: 通过连续调用 next 函数直到 hasNext 函数返回 false, next 函数返回值的次序应依次为: [1,3,2,4,5,6]。
拓展:假如给你 k 个一维向量呢?你的代码在这种情况下的扩展性又会如何呢?
拓展声明:
“锯齿” 顺序对于 k > 2 的情况定义可能会有些歧义。所以,假如你觉得 “锯齿” 这个表述不妥,也可以认为这是一种 “循环”。例如:
输入: [1,2,3] [4,5,6,7] [8,9] 输出: [1,4,8,2,5,9,3,6,7].
定义 vectors 列表保存输入的所有一维向量,indexes 表示 vectors 列表每一项当前所遍历到的下标位置,cur 表示当前遍历到的 vector 列表,而 size 表示 vectors 列表元素个数。具体实现参考以下代码实现。
public class ZigzagIterator { private int cur; private int size; private List<Integer> indexes = new ArrayList<>(); private List<List<Integer>> vectors = new ArrayList<>(); public ZigzagIterator(List<Integer> v1, List<Integer> v2) { cur = 0; size = 2; indexes.add(0); indexes.add(0); vectors.add(v1); vectors.add(v2); } public int next() { List<Integer> vector = vectors.get(cur); int index = indexes.get(cur); int res = vector.get(index); indexes.set(cur, index + 1); cur = (cur + 1) % size; return res; } public boolean hasNext() { int start = cur; while (indexes.get(cur) == vectors.get(cur).size()) { cur = (cur + 1) % size; if (start == cur) { return false; } } return true; } } /** * Your ZigzagIterator object will be instantiated and called as such: * ZigzagIterator i = new ZigzagIterator(v1, v2); * while (i.hasNext()) v[f()] = i.next(); */
给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target ,在 num 的数字之间添加 二元 运算符(不是一元)+、- 或 * ,返回 所有 能够得到 target 的表达式。
注意,返回表达式中的操作数 不应该 包含前导零。
示例 1:
输入: num = "123", target = 6 输出: ["1+2+3", "1*2*3"] 解释: “1*2*3” 和 “1+2+3” 的值都是6。
示例 2:
输入: num = "232", target = 8 输出: ["2*3+2", "2+3*2"] 解释: “2*3+2” 和 “2+3*2” 的值都是8。
示例 3:
输入: num = "3456237490", target = 9191 输出: [] 解释: 表达式 “3456237490” 无法得到 9191 。
提示:
1 <= num.length <= 10num 仅含数字-231 <= target <= 231 - 1class Solution { private List<String> ans; private String num; private int target; public List<String> addOperators(String num, int target) { ans = new ArrayList<>(); this.num = num; this.target = target; dfs(0, 0, 0, ""); return ans; } private void dfs(int u, long prev, long curr, String path) { if (u == num.length()) { if (curr == target) ans.add(path); return; } for (int i = u; i < num.length(); i++) { if (i != u && num.charAt(u) == '0') { break; } long next = Long.parseLong(num.substring(u, i + 1)); if (u == 0) { dfs(i + 1, next, next, path + next); } else { dfs(i + 1, next, curr + next, path + "+" + next); dfs(i + 1, -next, curr - next, path + "-" + next); dfs(i + 1, prev * next, curr - prev + prev * next, path + "*" + next); } } } }
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
示例 2:
输入: nums = [0] 输出: [0]
提示:
1 <= nums.length <= 104-231 <= nums[i] <= 231 - 1进阶:你能尽量减少完成的操作次数吗?
class Solution { public void moveZeroes(int[] nums) { int left = 0, n = nums.length; for (int right = 0; right < n; ++right) { if (nums[right] != 0) { int t = nums[left]; nums[left] = nums[right]; nums[right] = t; ++left; } } } }
请你在设计一个迭代器,在集成现有迭代器拥有的 hasNext 和 next 操作的基础上,还额外支持 peek 操作。
实现 PeekingIterator 类:
PeekingIterator(Iterator<int> nums) 使用指定整数迭代器 nums 初始化迭代器。int next() 返回数组中的下一个元素,并将指针移动到下个元素处。bool hasNext() 如果数组中存在下一个元素,返回 true ;否则,返回 false 。int peek() 返回数组中的下一个元素,但 不 移动指针。注意:每种语言可能有不同的构造函数和迭代器 Iterator,但均支持 int next() 和 boolean hasNext() 函数。
示例 1:
输入: ["PeekingIterator", "next", "peek", "next", "next", "hasNext"] [[[1, 2, 3]], [], [], [], [], []] 输出: [null, 1, 2, 2, 3, false] 解释: PeekingIterator peekingIterator = new PeekingIterator([1, 2, 3]); // [1,2,3] peekingIterator.next(); // 返回 1 ,指针移动到下一个元素 [1,2,3] peekingIterator.peek(); // 返回 2 ,指针未发生移动 [1,2,3] peekingIterator.next(); // 返回 2 ,指针移动到下一个元素 [1,2,3] peekingIterator.next(); // 返回 3 ,指针移动到下一个元素 [1,2,3] peekingIterator.hasNext(); // 返回 False
提示:
1 <= nums.length <= 10001 <= nums[i] <= 1000next 和 peek 的调用均有效next、hasNext 和 peek 最多调用 1000 次进阶:你将如何拓展你的设计?使之变得通用化,从而适应所有的类型,而不只是整数型?
定义一个变量 peekElement 专门用来保存下一个值,布尔变量 hasPeeked 标记是否保存了下一个元素。
// Java Iterator interface reference: // https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html class PeekingIterator implements Iterator<Integer> { private Iterator<Integer> iterator; private boolean hasPeeked; private Integer peekedElement; public PeekingIterator(Iterator<Integer> iterator) { // initialize any member here. this.iterator = iterator; } // Returns the next element in the iteration without advancing the iterator. public Integer peek() { if (!hasPeeked) { peekedElement = iterator.next(); hasPeeked = true; } return peekedElement; } // hasNext() and next() should behave the same as in the Iterator interface. // Override them if needed. @Override public Integer next() { if (!hasPeeked) { return iterator.next(); } Integer result = peekedElement; hasPeeked = false; peekedElement = null; return result; } @Override public boolean hasNext() { return hasPeeked || iterator.hasNext(); } }
给定一棵二叉搜索树和其中的一个节点 p ,找到该节点在树中的中序后继。如果节点没有中序后继,请返回 null 。
节点 p 的后继是值比 p.val 大的节点中键值最小的节点。
示例 1:

输入:root = [2,1,3], p = 1 输出:2 解释:这里 1 的中序后继是 2。请注意 p 和返回值都应是 TreeNode 类型。
示例 2:

输入:root = [5,3,6,2,4,null,null,1], p = 6 输出:null 解释:因为给出的节点没有中序后继,所以答案就返回 null 了。
提示:
[1, 104] 内。-105 <= Node.val <= 105利用二叉搜索树的特性,p 的中序后继一定是所有大于 p 的节点中最小的那个
class Solution { public TreeNode inorderSuccessor(TreeNode root, TreeNode p) { TreeNode cur = root, ans = null; while (cur != null) { if (cur.val <= p.val) { cur = cur.right; } else { ans = cur; cur = cur.left; } } return ans; } }
你被给定一个 m × n 的二维网格 rooms ,网格中有以下三种可能的初始化值:
-1 表示墙或是障碍物0 表示一扇门INF 无限表示一个空的房间。然后,我们用 231 - 1 = 2147483647 代表 INF。你可以认为通往门的距离总是小于 2147483647 的。你要给每个空房间位上填上该房间到 最近门的距离 ,如果无法到达门,则填 INF 即可。
示例 1:
输入:rooms = [[2147483647,-1,0,2147483647],[2147483647,2147483647,2147483647,-1],[2147483647,-1,2147483647,-1],[0,-1,2147483647,2147483647]] 输出:[[3,-1,0,1],[2,2,1,-1],[1,-1,2,-1],[0,-1,3,4]]
示例 2:
输入:rooms = [[-1]] 输出:[[-1]]
示例 3:
输入:rooms = [[2147483647]] 输出:[[2147483647]]
示例 4:
输入:rooms = [[0]] 输出:[[0]]
提示:
m == rooms.lengthn == rooms[i].length1 <= m, n <= 250rooms[i][j] 是 -1、0 或 231 - 1BFS。
将所有门放入队列,依次向外扩进行宽搜。由于宽度优先搜索保证我们在搜索 d + 1 距离的位置时, 距离为 d 的位置都已经被搜索过了,所以到达每一个房间的时候一定是最短距离。
class Solution { public void wallsAndGates(int[][] rooms) { int m = rooms.length; int n = rooms[0].length; Deque<int[]> q = new LinkedList<>(); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (rooms[i][j] == 0) { q.offer(new int[] {i, j}); } } } int d = 0; int[] dirs = {-1, 0, 1, 0, -1}; while (!q.isEmpty()) { ++d; for (int i = q.size(); i > 0; --i) { int[] p = q.poll(); for (int j = 0; j < 4; ++j) { int x = p[0] + dirs[j]; int y = p[1] + dirs[j + 1]; if (x >= 0 && x < m && y >= 0 && y < n && rooms[x][y] == Integer.MAX_VALUE) { rooms[x][y] = d; q.offer(new int[] {x, y}); } } } } } }
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 1:
输入:nums = [1,3,4,2,2] 输出:2
示例 2:
输入:nums = [3,1,3,4,2] 输出:3
提示:
1 <= n <= 105nums.length == n + 11 <= nums[i] <= nnums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次进阶:
nums 中至少存在一个重复的数字?O(n) 的解决方案吗?方法一:二分查找
如果值范围在 [1, mid] 的数小于等于 mid,说明此范围内没有重复的数,否则说明有重复数。
class Solution { public int findDuplicate(int[] nums) { int left = 1, right = nums.length - 1; while (left < right) { int mid = (left + right) >> 1; int cnt = 0; for (int v : nums) { if (v <= mid) { ++cnt; } } if (cnt > mid) { right = mid; } else { left = mid + 1; } } return left; } }
单词的 缩写 需要遵循 <起始字母><中间字母数><结尾字母> 这样的格式。如果单词只有两个字符,那么它就是它自身的 缩写 。
以下是一些单词缩写的范例:
dog --> d1g 因为第一个字母 'd' 和最后一个字母 'g' 之间有 1 个字母internationalization --> i18n 因为第一个字母 'i' 和最后一个字母 'n' 之间有 18 个字母it --> it 单词只有两个字符,它就是它自身的 缩写实现 ValidWordAbbr 类:
ValidWordAbbr(String[] dictionary) 使用单词字典 dictionary 初始化对象boolean isUnique(string word) 如果满足下述任意一个条件,返回 true ;否则,返回 false :
dictionary 中没有任何其他单词的 缩写 与该单词 word 的 缩写 相同。dictionary 中的所有 缩写 与该单词 word 的 缩写 相同的单词都与 word 相同 。示例:
输入
["ValidWordAbbr", "isUnique", "isUnique", "isUnique", "isUnique", "isUnique"]
[[["deer", "door", "cake", "card"]], ["dear"], ["cart"], ["cane"], ["make"], ["cake"]]
输出
[null, false, true, false, true, true]
解释
ValidWordAbbr validWordAbbr = new ValidWordAbbr(["deer", "door", "cake", "card"]);
validWordAbbr.isUnique("dear"); // 返回 false,字典中的 "deer" 与输入 "dear" 的缩写都是 "d2r",但这两个单词不相同
validWordAbbr.isUnique("cart"); // 返回 true,字典中不存在缩写为 "c2t" 的单词
validWordAbbr.isUnique("cane"); // 返回 false,字典中的 "cake" 与输入 "cane" 的缩写都是 "c2e",但这两个单词不相同
validWordAbbr.isUnique("make"); // 返回 true,字典中不存在缩写为 "m2e" 的单词
validWordAbbr.isUnique("cake"); // 返回 true,因为 "cake" 已经存在于字典中,并且字典中没有其他缩写为 "c2e" 的单词
提示:
1 <= dictionary.length <= 3 * 1041 <= dictionary[i].length <= 20dictionary[i] 由小写英文字母组成1 <= word <= 20word 由小写英文字母组成5000 次 isUnique哈希表实现,其中 key 存放单词缩写,value 存放单词缩写所对应的所有单词的集合。
class ValidWordAbbr { private Map<String, Set<String>> words; public ValidWordAbbr(String[] dictionary) { words = new HashMap<>(); for (String word : dictionary) { String abbr = abbr(word); words.computeIfAbsent(abbr, k -> new HashSet<>()).add(word); } } public boolean isUnique(String word) { String abbr = abbr(word); Set<String> vals = words.get(abbr); return vals == null || (vals.size() == 1 && vals.contains(word)); } private String abbr(String s) { int n = s.length(); return n < 3 ? s : s.charAt(0) + Integer.toString(n - 2) + s.charAt(n - 1); } } /** * Your ValidWordAbbr object will be instantiated and called as such: * ValidWordAbbr obj = new ValidWordAbbr(dictionary); * boolean param_1 = obj.isUnique(word); */
根据 百度百科 , 生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。
示例 1:
输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] 输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
示例 2:
输入:board = [[1,1],[1,0]] 输出:[[1,1],[1,1]]
提示:
m == board.lengthn == board[i].length1 <= m, n <= 25board[i][j] 为 0 或 1进阶:
方法一:原地标记
我们不妨定义两个新的状态,其中状态 表示活细胞在下一个状态转为死细胞,状态 表示死细胞在下一个状态转为活细胞。那么,对于当前遍历到的格子,如果格子大于 ,就表示当前格子是活细胞,否则就是死细胞。
因此,我们可以遍历整个面板,对于每个格子,统计其周围的活细胞数目,用变量 表示。如果当前格子是活细胞,那么当 或者 时,当前格子的下一个状态是死细胞,即状态 ;如果当前格子是死细胞,那么当 时,当前格子的下一个状态是活细胞,即状态 。
最后,我们再遍历一遍面板,将状态 的格子更新为死细胞,将状态 的格子更新为活细胞。
时间复杂度 ,其中 和 分别是面板的行数和列数。我们需要遍历整个面板。空间复杂度 。
class Solution { public void gameOfLife(int[][] board) { int m = board.length, n = board[0].length; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { int live = -board[i][j]; for (int x = i - 1; x <= i + 1; ++x) { for (int y = j - 1; y <= j + 1; ++y) { if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] > 0) { ++live; } } } if (board[i][j] == 1 && (live < 2 || live > 3)) { board[i][j] = 2; } if (board[i][j] == 0 && live == 3) { board[i][j] = -1; } } } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == 2) { board[i][j] = 0; } else if (board[i][j] == -1) { board[i][j] = 1; } } } } }
给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。
示例1:
输入: pattern = "abba", s = "dog cat cat dog" 输出: true
示例 2:
输入:pattern = "abba", s = "dog cat cat fish" 输出: false
示例 3:
输入: pattern = "aaaa", s = "dog cat cat dog" 输出: false
提示:
1 <= pattern.length <= 300pattern 只包含小写英文字母1 <= s.length <= 3000s 只包含小写英文字母和 ' 's 不包含 任何前导或尾随对空格s 中每个单词都被 单个空格 分隔方法一:哈希表
我们先将字符串 按照空格分割成单词数组 ,如果 和 的长度不相等,直接返回 false。否则,我们使用两个哈希表 和 ,分别记录 和 中每个字符和单词的对应关系。
接下来,我们遍历 和 ,对于每个字符 和单词 ,如果 中存在 的映射,且映射的单词不是 ,或者 中存在 的映射,且映射的字符不是 ,则返回 false。否则,我们将 和 的映射分别加入 和 中。
遍历结束后,返回 true。
时间复杂度 ,空间复杂度 。其中 和 分别是 和字符串 的长度。
class Solution { public boolean wordPattern(String pattern, String s) { String[] ws = s.split(" "); if (pattern.length() != ws.length) { return false; } Map<Character, String> d1 = new HashMap<>(); Map<String, Character> d2 = new HashMap<>(); for (int i = 0; i < ws.length; ++i) { char a = pattern.charAt(i); String b = ws[i]; if (!d1.getOrDefault(a, b).equals(b) || d2.getOrDefault(b, a) != a) { return false; } d1.put(a, b); d2.put(b, a); } return true; } }
给你一种规律 pattern 和一个字符串 s,请你判断 s 是否和 pattern 的规律相匹配。
如果存在单个字符到字符串的 双射映射 ,那么字符串 s 匹配 pattern ,即:如果pattern 中的每个字符都被它映射到的字符串替换,那么最终的字符串则为 s 。双射 意味着映射双方一一对应,不会存在两个字符映射到同一个字符串,也不会存在一个字符分别映射到两个不同的字符串。
示例 1:
输入:pattern = "abab", s = "redblueredblue" 输出:true 解释:一种可能的映射如下: 'a' -> "red" 'b' -> "blue"
示例 2:
输入:pattern = "aaaa", s = "asdasdasdasd" 输出:true 解释:一种可能的映射如下: 'a' -> "asd"
示例 3:
输入:pattern = "aabb", s = "xyzabcxzyabc" 输出:false
提示:
1 <= pattern.length, s.length <= 20pattern 和 s 由小写英文字母组成class Solution { private Set<String> vis; private Map<Character, String> d; private String p; private String s; private int m; private int n; public boolean wordPatternMatch(String pattern, String s) { vis = new HashSet<>(); d = new HashMap<>(); this.p = pattern; this.s = s; m = p.length(); n = s.length(); return dfs(0, 0); } private boolean dfs(int i, int j) { if (i == m && j == n) { return true; } if (i == m || j == n || m - i > n - j) { return false; } char c = p.charAt(i); for (int k = j + 1; k <= n; ++k) { String t = s.substring(j, k); if (d.getOrDefault(c, "").equals(t)) { if (dfs(i + 1, k)) { return true; } } if (!d.containsKey(c) && !vis.contains(t)) { d.put(c, t); vis.add(t); if (dfs(i + 1, k)) { return true; } vis.remove(t); d.remove(c); } } return false; } }
你和你的朋友,两个人一起玩 Nim 游戏:
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。
示例 1:
输入:n = 4 输出:false 解释:以下是可能的结果: 1. 移除1颗石头。你的朋友移走了3块石头,包括最后一块。你的朋友赢了。 2. 移除2个石子。你的朋友移走2块石头,包括最后一块。你的朋友赢了。 3.你移走3颗石子。你的朋友移走了最后一块石头。你的朋友赢了。 在所有结果中,你的朋友是赢家。
示例 2:
输入:n = 1 输出:true
示例 3:
输入:n = 2 输出:true
提示:
1 <= n <= 231 - 1方法一:数学推理
第一个得到 的倍数(即 能被 整除)的将会输掉比赛。
证明:
时间复杂度 。
class Solution { public boolean canWinNim(int n) { return n % 4 != 0; } }
你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下:
给你一个字符串 currentState ,其中只含 '+' 和 '-' 。你和朋友轮流将 连续 的两个 "++" 反转成 "--" 。当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。
计算并返回 一次有效操作 后,字符串 currentState 所有的可能状态,返回结果可以按 任意顺序 排列。如果不存在可能的有效操作,请返回一个空列表 [] 。
示例 1:
输入:currentState = "++++" 输出:["--++","+--+","++--"]
示例 2:
输入:currentState = "+" 输出:[]
提示:
1 <= currentState.length <= 500currentState[i] 不是 '+' 就是 '-'class Solution { public List<String> generatePossibleNextMoves(String currentState) { char[] cs = currentState.toCharArray(); List<String> ans = new ArrayList<>(); for (int i = 0; i < cs.length - 1; ++i) { if (cs[i] == '+' && cs[i + 1] == '+') { cs[i] = '-'; cs[i + 1] = '-'; ans.add(String.valueOf(cs)); cs[i] = '+'; cs[i + 1] = '+'; } } return ans; } }
你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下:
给你一个字符串 currentState ,其中只含 '+' 和 '-' 。你和朋友轮流将 连续 的两个 "++" 反转成 "--" 。当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。默认每个人都会采取最优策略。
请你写出一个函数来判定起始玩家 是否存在必胜的方案 :如果存在,返回 true ;否则,返回 false 。
示例 1:
输入:currentState = "++++" 输出:true 解释:起始玩家可将中间的 "++" 翻转变为 "+--+" 从而得胜。
示例 2:
输入:currentState = "+" 输出:false
提示:
1 <= currentState.length <= 60currentState[i] 不是 '+' 就是 '-'进阶:请推导你算法的时间复杂度。
方法一:状态压缩 + 记忆化搜索
方法二:Sprague-Grundy 定理
Sprague-Grundy 定理为游戏的每一个状态定义了一个 Sprague-Grundy 数(简称 SG 数),游戏状态的组合相当于 SG 数的异或运算。
Sprague-Grundy 定理的完整表述如下:
若一个游戏满足以下条件:
SG 数有如下性质:
时间复杂度 。
class Solution { private int n; private Map<Long, Boolean> memo = new HashMap<>(); public boolean canWin(String currentState) { long mask = 0; n = currentState.length(); for (int i = 0; i < n; ++i) { if (currentState.charAt(i) == '+') { mask |= 1 << i; } } return dfs(mask); } private boolean dfs(long mask) { if (memo.containsKey(mask)) { return memo.get(mask); } for (int i = 0; i < n - 1; ++i) { if ((mask & (1 << i)) == 0 || (mask & (1 << (i + 1))) == 0) { continue; } if (dfs(mask ^ (1 << i) ^ (1 << (i + 1)))) { continue; } memo.put(mask, true); return true; } memo.put(mask, false); return false; } }
class Solution { private int n; private int[] sg; public boolean canWin(String currentState) { n = currentState.length(); sg = new int[n + 1]; Arrays.fill(sg, -1); int i = 0; int ans = 0; while (i < n) { int j = i; while (j < n && currentState.charAt(j) == '+') { ++j; } ans ^= win(j - i); i = j + 1; } return ans > 0; } private int win(int i) { if (sg[i] != -1) { return sg[i]; } boolean[] vis = new boolean[n]; for (int j = 0; j < i - 1; ++j) { vis[win(j) ^ win(i - j - 2)] = true; } for (int j = 0; j < n; ++j) { if (!vis[j]) { sg[i] = j; return j; } } return 0; } }
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
arr = [2,3,4] 的中位数是 3 。arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
示例 1:
输入 ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []] 输出 [null, null, null, 1.5, null, 2.0] 解释 MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // arr = [1] medianFinder.addNum(2); // arr = [1, 2] medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2) medianFinder.addNum(3); // arr[1, 2, 3] medianFinder.findMedian(); // return 2.0
提示:
-105 <= num <= 105findMedian 之前,数据结构中至少有一个元素5 * 104 次调用 addNum 和 findMedian方法一:优先队列(双堆)
创建大根堆、小根堆,其中:大根堆存放较小的一半元素,小根堆存放较大的一半元素。
添加元素时,先放入小根堆,然后将小根堆对顶元素弹出并放入大根堆(使得大根堆个数多 );若大小根堆元素个数差超过 ,则将大根堆元素弹出放入小根堆。
取中位数时,若大根堆元素较多,取大根堆堆顶,否则取两堆顶元素和的平均值。
时间复杂度分析:
每次添加元素的时间复杂度为 ,取中位数的时间复杂度为 。
class MedianFinder { private PriorityQueue<Integer> q1 = new PriorityQueue<>(); private PriorityQueue<Integer> q2 = new PriorityQueue<>(Collections.reverseOrder()); /** initialize your data structure here. */ public MedianFinder() { } public void addNum(int num) { q1.offer(num); q2.offer(q1.poll()); if (q2.size() - q1.size() > 1) { q1.offer(q2.poll()); } } public double findMedian() { if (q2.size() > q1.size()) { return q2.peek(); } return (q1.peek() + q2.peek()) * 1.0 / 2; } } /** * Your MedianFinder object will be instantiated and called as such: * MedianFinder obj = new MedianFinder(); * obj.addNum(num); * double param_2 = obj.findMedian(); */
给你一个 m x n 的二进制网格 grid ,其中 1 表示某个朋友的家所处的位置。返回 最小的 总行走距离 。
总行走距离 是朋友们家到碰头地点的距离之和。
我们将使用 曼哈顿距离 来计算,其中 distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y| 。
示例 1:

输入: grid = [[1,0,0,0,1],[0,0,0,0,0],[0,0,1,0,0]] 输出: 6 解释: 给定的三个人分别住在(0,0),(0,4) 和 (2,2): (0,2) 是一个最佳的碰面点,其总行走距离为 2 + 2 + 2 = 6,最小,因此返回 6。
示例 2:
输入: grid = [[1,1]] 输出: 1
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 200grid[i][j] == 0 or 1.grid 中 至少 有两个朋友方法一:排序 + 中位数
对于每一行,我们可以将所有的 的下标排序,然后取中位数 作为碰头地点的横坐标。
对于每一列,我们可以将所有的 的下标排序,然后取中位数 作为碰头地点的纵坐标。
最后,我们计算所有 到碰头地点 的曼哈顿距离之和即可。
时间复杂度 。最多有 个 ,排序的时间复杂度为 。
相似题目:
class Solution { public int minTotalDistance(int[][] grid) { int m = grid.length, n = grid[0].length; List<Integer> rows = new ArrayList<>(); List<Integer> cols = new ArrayList<>(); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == 1) { rows.add(i); cols.add(j); } } } Collections.sort(cols); int i = rows.get(rows.size() >> 1); int j = cols.get(cols.size() >> 1); return f(rows, i) + f(cols, j); } private int f(List<Integer> arr, int x) { int s = 0; for (int v : arr) { s += Math.abs(v - x); } return s; } }
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
示例 1:
输入:root = [1,2,3,null,null,4,5] 输出:[1,2,3,null,null,4,5]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [1] 输出:[1]
示例 4:
输入:root = [1,2] 输出:[1,2]
提示:
[0, 104] 内-1000 <= Node.val <= 1000public class Codec { private static final String NULL = "#"; private static final String SEP = ","; // Encodes a tree to a single string. public String serialize(TreeNode root) { if (root == null) { return ""; } StringBuilder sb = new StringBuilder(); preorder(root, sb); return sb.toString(); } private void preorder(TreeNode root, StringBuilder sb) { if (root == null) { sb.append(NULL + SEP); return; } sb.append(root.val + SEP); preorder(root.left, sb); preorder(root.right, sb); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data == null || "".equals(data)) { return null; } List<String> vals = new LinkedList<>(); for (String x : data.split(SEP)) { vals.add(x); } return deserialize(vals); } private TreeNode deserialize(List<String> vals) { String first = vals.remove(0); if (NULL.equals(first)) { return null; } TreeNode root = new TreeNode(Integer.parseInt(first)); root.left = deserialize(vals); root.right = deserialize(vals); return root; } } // Your Codec object will be instantiated and called as such: // Codec ser = new Codec(); // Codec deser = new Codec(); // TreeNode ans = deser.deserialize(ser.serialize(root));
给你一棵指定的二叉树的根节点 root ,请你计算其中 最长连续序列路径 的长度。
最长连续序列路径 是依次递增 1 的路径。该路径,可以是从某个初始节点到树中任意节点,通过「父 - 子」关系连接而产生的任意路径。且必须从父节点到子节点,反过来是不可以的。
示例 1:
输入:root = [1,null,3,2,4,null,null,null,5] 输出:3 解释:当中,最长连续序列是 3-4-5 ,所以返回结果为 3 。
示例 2:
输入:root = [2,null,3,2,null,1] 输出:2 解释:当中,最长连续序列是 2-3 。注意,不是 3-2-1,所以返回 2 。
提示:
[1, 3 * 104] 内-3 * 104 <= Node.val <= 3 * 104DFS。
class Solution { private int ans; public int longestConsecutive(TreeNode root) { ans = 1; dfs(root, null, 1); return ans; } private void dfs(TreeNode root, TreeNode p, int t) { if (root == null) { return; } t = p != null && p.val + 1 == root.val ? t + 1 : 1; ans = Math.max(ans, t); dfs(root.left, root, t); dfs(root.right, root, t); } }
你在和朋友一起玩 猜数字(Bulls and Cows)游戏,该游戏规则如下:
写出一个秘密数字,并请朋友猜这个数字是多少。朋友每猜测一次,你就会给他一个包含下述信息的提示:
给你一个秘密数字 secret 和朋友猜测的数字 guess ,请你返回对朋友这次猜测的提示。
提示的格式为 "xAyB" ,x 是公牛个数, y 是奶牛个数,A 表示公牛,B 表示奶牛。
请注意秘密数字和朋友猜测的数字都可能含有重复数字。
示例 1:
输入:secret = "1807", guess = "7810" 输出:"1A3B" 解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。 "1807" | "7810"
示例 2:
输入:secret = "1123", guess = "0111" 输出:"1A1B" 解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。 "1123" "1123" | or | "0111" "0111" 注意,两个不匹配的 1 中,只有一个会算作奶牛(数字猜对位置不对)。通过重新排列非公牛数字,其中仅有一个 1 可以成为公牛数字。
提示:
1 <= secret.length, guess.length <= 1000secret.length == guess.lengthsecret 和 guess 仅由数字组成class Solution { public String getHint(String secret, String guess) { int x = 0, y = 0; int[] cnt1 = new int[10]; int[] cnt2 = new int[10]; for (int i = 0; i < secret.length(); ++i) { int a = secret.charAt(i) - '0', b = guess.charAt(i) - '0'; if (a == b) { ++x; } else { ++cnt1[a]; ++cnt2[b]; } } for (int i = 0; i < 10; ++i) { y += Math.min(cnt1[i], cnt2[i]); } return String.format("%dA%dB", x, y); } }
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3] 输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7] 输出:1
提示:
1 <= nums.length <= 2500-104 <= nums[i] <= 104进阶:
O(n log(n)) 吗?方法一:动态规划
定义 dp[i] 为以 nums[i] 结尾的最长子序列的长度,dp[i] 初始化为 1(i∈[0, n))。即题目求的是 dp[i] (i ∈[0, n-1])的最大值。
状态转移方程为:dp[i] = max(dp[j]) + 1,其中 0≤j<i 且 nums[j] < nums[i]。
时间复杂度:。
方法二:贪心 + 二分查找
维护一个数组 d[i],表示长度为 i 的最长上升子序列末尾元素的最小值,初始值 d[1] = nums[0]。
直观上,d[i] 是单调递增数组。
证明:假设存在 d[j] ≥ d[i],且 j < i,我们考虑从长度为 i 的最长上升子序列的末尾删除 i - j 个元素,那么这个序列长度变为 j,且第 j 个元素 d[j] 必然小于 d[i],由于前面假设 d[j] ≥ d[i],产生了矛盾,因此数组 d 是单调递增数组。
算法思路:
设当前求出的最长上升子序列的长度为 size,初始 size = 1,从前往后遍历数组 nums,在遍历到 nums[i] 时:
nums[i] > d[size],则直接将 nums[i] 加入到数组 d 的末尾,并且更新 size 自增;nums[i] 的位置 idx,更新 d[idx] = nums[i]。最终返回 size。
时间复杂度:。
方法三:树状数组
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 x 位置的数加上一个值 delta;query(x):查询序列 [1,...x] 区间的区间和,即位置 x 的前缀和。这两个操作的时间复杂度均为 。当数的范围比较大时,需要进行离散化,即先进行去重并排序,然后对每个数字进行编号。
本题我们使用树状数组 tree[x] 来维护以 x 结尾的最长上升子序列的长度。
时间复杂度:。
动态规划:
贪心 + 二分查找:
树状数组:
动态规划:
class Solution { public int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; Arrays.fill(dp, 1); int res = 1; for (int i = 1; i < n; ++i) { for (int j = 0; j < i; ++j) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } res = Math.max(res, dp[i]); } return res; } }
贪心 + 二分查找:
class Solution { public int lengthOfLIS(int[] nums) { int n = nums.length; int[] d = new int[n + 1]; d[1] = nums[0]; int size = 1; for (int i = 1; i < n; ++i) { if (nums[i] > d[size]) { d[++size] = nums[i]; } else { int left = 1, right = size; while (left < right) { int mid = (left + right) >> 1; if (d[mid] >= nums[i]) { right = mid; } else { left = mid + 1; } } int p = d[left] >= nums[i] ? left : 1; d[p] = nums[i]; } } return size; } }
树状数组:
class Solution { public int lengthOfLIS(int[] nums) { TreeSet<Integer> ts = new TreeSet(); for (int v : nums) { ts.add(v); } int idx = 1; Map<Integer, Integer> m = new HashMap<>(); for (int v : ts) { m.put(v, idx++); } BinaryIndexedTree tree = new BinaryIndexedTree(m.size()); int ans = 1; for (int v : nums) { int x = m.get(v); int t = tree.query(x - 1) + 1; ans = Math.max(ans, t); tree.update(x, t); } return ans; } } class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int val) { while (x <= n) { c[x] = Math.max(c[x], val); x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s = Math.max(s, c[x]); x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } }
动态规划:
贪心 + 二分查找:
动态规划:
贪心 + 二分查找:
树状数组:
动态规划:
贪心 + 二分查找:
树状数组:
给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。
示例 1:
输入:s = "()())()" 输出:["(())()","()()()"]
示例 2:
输入:s = "(a)())()" 输出:["(a())()","(a)()()"]
示例 3:
输入:s = ")("
输出:[""]
提示:
1 <= s.length <= 25s 由小写英文字母以及括号 '(' 和 ')' 组成s 中至多含 20 个括号方法一:DFS + 剪枝
我们首先处理得到字符串 待删除的左、右括号的最小数量,分别记为 和 。
然后我们设计一个递归函数 dfs(i, l, r, lcnt, rcnt, t),其中:
i 表示当前处理到字符串 的第 个字符;l 和 r 分别表示剩余待删除的左、右括号的数量;t 表示当前得到的字符串;lcnt 和 rcnt 分别表示当前得到的字符串中左、右括号的数量。递归函数的逻辑如下:
i 等于字符串 的长度,且 l 和 r 都等于 ,则将 t 加入答案数组中;dfs(i+1, l-1, r, lcnt, rcnt, t);dfs(i+1, l, r-1, lcnt, rcnt, t);lcnt,如果是右括号,我们需要更新 rcnt,然后递归调用 dfs(i+1, l, r, lcnt, rcnt, t+s[i])。我们调用 dfs(0, l, r, 0, 0, ""),搜索所有可能的字符串。
最后返回去重后的答案数组即可。
时间复杂度 ,空间复杂度 。长度为 的字符串有 种可能的删除方式,每种删除方式需要 的时间复制字符串。因此总时间复杂度为 。
class Solution { private String s; private int n; private Set<String> ans = new HashSet<>(); public List<String> removeInvalidParentheses(String s) { this.s = s; this.n = s.length(); int l = 0, r = 0; for (char c : s.toCharArray()) { if (c == '(') { ++l; } else if (c == ')') { if (l > 0) { --l; } else { ++r; } } } dfs(0, l, r, 0, 0, ""); return new ArrayList<>(ans); } private void dfs(int i, int l, int r, int lcnt, int rcnt, String t) { if (i == n) { if (l == 0 && r == 0) { ans.add(t); } return; } if (n - i < l + r || lcnt < rcnt) { return; } char c = s.charAt(i); if (c == '(' && l > 0) { dfs(i + 1, l - 1, r, lcnt, rcnt, t); } if (c == ')' && r > 0) { dfs(i + 1, l, r - 1, lcnt, rcnt, t); } int x = c == '(' ? 1 : 0; int y = c == ')' ? 1 : 0; dfs(i + 1, l, r, lcnt + x, rcnt + y, t + c); } }
图片在计算机处理中往往是使用二维矩阵来表示的。
给你一个大小为 m x n 的二进制矩阵 image 表示一张黑白图片,0 代表白色像素,1 代表黑色像素。
黑色像素相互连接,也就是说,图片中只会有一片连在一块儿的黑色像素。像素点是水平或竖直方向连接的。
给你两个整数 x 和 y 表示某一个黑色像素的位置,请你找出包含全部黑色像素的最小矩形(与坐标轴对齐),并返回该矩形的面积。
你必须设计并实现一个时间复杂度低于 O(mn) 的算法来解决此问题。
示例 1:
输入:image = [["0","0","1","0"],["0","1","1","0"],["0","1","0","0"]], x = 0, y = 2 输出:6
示例 2:
输入:image = [["1"]], x = 0, y = 0 输出:1
提示:
m == image.lengthn == image[i].length1 <= m, n <= 100image[i][j] 为 '0' 或 '1'1 <= x < m1 <= y < nimage[x][y] == '1'image 中的黑色像素仅形成一个 组件二分查找,时间复杂度 O(mlogn + nlogm)。
class Solution { public int minArea(char[][] image, int x, int y) { int m = image.length, n = image[0].length; int left = 0, right = x; while (left < right) { int mid = (left + right) >> 1; int c = 0; while (c < n && image[mid][c] == '0') { ++c; } if (c < n) { right = mid; } else { left = mid + 1; } } int u = left; left = x; right = m - 1; while (left < right) { int mid = (left + right + 1) >> 1; int c = 0; while (c < n && image[mid][c] == '0') { ++c; } if (c < n) { left = mid; } else { right = mid - 1; } } int d = left; left = 0; right = y; while (left < right) { int mid = (left + right) >> 1; int r = 0; while (r < m && image[r][mid] == '0') { ++r; } if (r < m) { right = mid; } else { left = mid + 1; } } int l = left; left = y; right = n - 1; while (left < right) { int mid = (left + right + 1) >> 1; int r = 0; while (r < m && image[r][mid] == '0') { ++r; } if (r < m) { left = mid; } else { right = mid - 1; } } int r = left; return (d - u + 1) * (r - l + 1); } }
给定一个整数数组 nums,处理以下类型的多个查询:
left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right实现 NumArray 类:
NumArray(int[] nums) 使用数组 nums 初始化对象int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )示例 1:
输入: ["NumArray", "sumRange", "sumRange", "sumRange"] [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]] 输出: [null, 1, -1, -3] 解释: NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]); numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3) numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
提示:
1 <= nums.length <= 104-105 <= nums[i] <= 1050 <= i <= j < nums.length104 次 sumRange 方法方法一:前缀和
前缀和计算公式:s[i + 1] = s[i] + nums[i]。
初始化的时间复杂度是 ,每次查询的时间复杂度是 。其中 是数组的长度。
class NumArray { private int[] s; public NumArray(int[] nums) { int n = nums.length; s = new int[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } } public int sumRange(int left, int right) { return s[right + 1] - s[left]; } } /** * Your NumArray object will be instantiated and called as such: * NumArray obj = new NumArray(nums); * int param_1 = obj.sumRange(left,right); */
给定一个二维矩阵 matrix,以下类型的多个请求:
(row1, col1) ,右下角 为 (row2, col2) 。实现 NumMatrix 类:
NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。示例 1:

输入: ["NumMatrix","sumRegion","sumRegion","sumRegion"] [[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]] 输出: [null, 8, 11, 12] 解释: NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]); numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和) numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和) numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 200-105 <= matrix[i][j] <= 1050 <= row1 <= row2 < m0 <= col1 <= col2 < n104 次 sumRegion 方法方法一:二维前缀和
我们用 表示第 行第 列左上部分所有元素之和,下标 和 均从 开始。可以得到以下前缀和公式:
那么分别以 和 为左上角和右下角的矩形的元素之和为:
我们在初始化方法中预处理出前缀和数组 ,在查询方法中直接返回上述公式的结果即可。
初始化的时间复杂度为 ,查询的时间复杂度为 。
class NumMatrix { private int[][] s; public NumMatrix(int[][] matrix) { int m = matrix.length, n = matrix[0].length; s = new int[m + 1][n + 1]; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + matrix[i][j]; } } } public int sumRegion(int row1, int col1, int row2, int col2) { return s[row2 + 1][col2 + 1] - s[row2 + 1][col1] - s[row1][col2 + 1] + s[row1][col1]; } } /** * Your NumMatrix object will be instantiated and called as such: * NumMatrix obj = new NumMatrix(matrix); * int param_1 = obj.sumRegion(row1,col1,row2,col2); */
给你一个大小为 m x n 的二进制网格 grid 。网格表示一个地图,其中,0 表示水,1 表示陆地。最初,grid 中的所有单元格都是水单元格(即,所有单元格都是 0)。
可以通过执行 addLand 操作,将某个位置的水转换成陆地。给你一个数组 positions ,其中 positions[i] = [ri, ci] 是要执行第 i 次操作的位置 (ri, ci) 。
返回一个整数数组 answer ,其中 answer[i] 是将单元格 (ri, ci) 转换为陆地后,地图中岛屿的数量。
岛屿 的定义是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。
示例 1:
输入:m = 3, n = 3, positions = [[0,0],[0,1],[1,2],[2,1]] 输出:[1,1,2,3] 解释: 起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」) - 操作 #1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。此时存在 1 个岛屿。 - 操作 #2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。此时存在 1 个岛屿。 - 操作 #3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。此时存在 2 个岛屿。 - 操作 #4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。此时存在 3 个岛屿。
示例 2:
输入:m = 1, n = 1, positions = [[0,0]] 输出:[1]
提示:
1 <= m, n, positions.length <= 1041 <= m * n <= 104positions[i].length == 20 <= ri < m0 <= ci < n进阶:你可以设计一个时间复杂度 O(k log(mn)) 的算法解决此问题吗?(其中 k == positions.length)
并查集。
并查集模板:
模板 1——朴素并查集:
模板 2——维护 size 的并查集:
模板 3——维护到祖宗节点距离的并查集:
class Solution { private int[] p; public List<Integer> numIslands2(int m, int n, int[][] positions) { p = new int[m * n]; for (int i = 0; i < p.length; ++i) { p[i] = i; } int[][] grid = new int[m][n]; int cnt = 0; List<Integer> ans = new ArrayList<>(); int[] dirs = {-1, 0, 1, 0, -1}; for (int[] pos : positions) { int i = pos[0]; int j = pos[1]; if (grid[i][j] == 1) { ans.add(cnt); continue; } grid[i][j] = 1; ++cnt; for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1 && find(x * n + y) != find(i * n + j)) { p[find(x * n + y)] = find(i * n + j); --cnt; } } ans.add(cnt); } return ans; } private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; } }
累加数 是一个字符串,组成它的数字可以形成累加序列。
一个有效的 累加序列 必须 至少 包含 3 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。
给你一个只包含数字 '0'-'9' 的字符串,编写一个算法来判断给定输入是否是 累加数 。如果是,返回 true ;否则,返回 false 。
说明:累加序列里的数,除数字 0 之外,不会 以 0 开头,所以不会出现 1, 2, 03 或者 1, 02, 3 的情况。
示例 1:
输入:"112358" 输出:true 解释:累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
示例 2:
输入:"199100199"
输出:true
解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199
提示:
1 <= num.length <= 35num 仅由数字(0 - 9)组成进阶:你计划如何处理由过大的整数输入导致的溢出?
DFS + 剪枝。
Python 大整数相加不会有溢出问题。由于 num 字符串长度最大为 35,因此对于其他语言,可以通过控制整数长度防止溢出。
class Solution { public boolean isAdditiveNumber(String num) { int n = num.length(); for (int i = 1; i < Math.min(n - 1, 19); ++i) { for (int j = i + 1; j < Math.min(n, i + 19); ++j) { if (i > 1 && num.charAt(0) == '0') { break; } if (j - i > 1 && num.charAt(i) == '0') { continue; } long a = Long.parseLong(num.substring(0, i)); long b = Long.parseLong(num.substring(i, j)); if (dfs(a, b, num.substring(j))) { return true; } } } return false; } private boolean dfs(long a, long b, String num) { if ("".equals(num)) { return true; } if (a + b > 0 && num.charAt(0) == '0') { return false; } for (int i = 1; i < Math.min(num.length() + 1, 19); ++i) { if (a + b == Long.parseLong(num.substring(0, i))) { if (dfs(b, a + b, num.substring(i))) { return true; } } } return false; } }
给你一个数组 nums ,请你完成两类查询。
nums 下标对应的值nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right实现 NumArray 类:
NumArray(int[] nums) 用整数数组 nums 初始化对象void update(int index, int val) 将 nums[index] 的值 更新 为 valint sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 (即,nums[left] + nums[left + 1], ..., nums[right])示例 1:
输入: ["NumArray", "sumRange", "update", "sumRange"] [[[1, 3, 5]], [0, 2], [1, 2], [0, 2]] 输出: [null, 9, null, 8] 解释: NumArray numArray = new NumArray([1, 3, 5]); numArray.sumRange(0, 2); // 返回 1 + 3 + 5 = 9 numArray.update(1, 2); // nums = [1,2,5] numArray.sumRange(0, 2); // 返回 1 + 2 + 5 = 8
提示:
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 1000 <= index < nums.length-100 <= val <= 1000 <= left <= right < nums.lengthupdate 和 sumRange 方法次数不大于 3 * 104 方法一:树状数组
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 x 位置的数加上一个值 delta;query(x):查询序列 [1,...x] 区间的区间和,即位置 x 的前缀和。这两个操作的时间复杂度均为 。
方法二:线段树
线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。
[1, N];[x, x];[l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r], 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。树状数组:
线段树:
树状数组:
class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } } class NumArray { private BinaryIndexedTree tree; public NumArray(int[] nums) { int n = nums.length; tree = new BinaryIndexedTree(n); for (int i = 0; i < n; ++i) { tree.update(i + 1, nums[i]); } } public void update(int index, int val) { int prev = sumRange(index, index); tree.update(index + 1, val - prev); } public int sumRange(int left, int right) { return tree.query(right + 1) - tree.query(left); } } /** * Your NumArray object will be instantiated and called as such: * NumArray obj = new NumArray(nums); * obj.update(index,val); * int param_2 = obj.sumRange(left,right); */
线段树:
class Node { int l; int r; int v; } class SegmentTree { private Node[] tr; private int[] nums; public SegmentTree(int[] nums) { this.nums = nums; int n = nums.length; tr = new Node[n << 2]; for (int i = 0; i < tr.length; ++i) { tr[i] = new Node(); } build(1, 1, n); } public void build(int u, int l, int r) { tr[u].l = l; tr[u].r = r; if (l == r) { tr[u].v = nums[l - 1]; return; } int mid = (l + r) >> 1; build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r); pushup(u); } public void modify(int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) { tr[u].v = v; return; } int mid = (tr[u].l + tr[u].r) >> 1; if (x <= mid) { modify(u << 1, x, v); } else { modify(u << 1 | 1, x, v); } pushup(u); } public int query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) { return tr[u].v; } int mid = (tr[u].l + tr[u].r) >> 1; int v = 0; if (l <= mid) { v += query(u << 1, l, r); } if (r > mid) { v += query(u << 1 | 1, l, r); } return v; } public void pushup(int u) { tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v; } } class NumArray { private SegmentTree tree; public NumArray(int[] nums) { tree = new SegmentTree(nums); } public void update(int index, int val) { tree.modify(1, index + 1, val); } public int sumRange(int left, int right) { return tree.query(1, left + 1, right + 1); } } /** * Your NumArray object will be instantiated and called as such: * NumArray obj = new NumArray(nums); * obj.update(index,val); * int param_2 = obj.sumRange(left,right); */
树状数组:
线段树:
树状数组:
给你一个二维矩阵 matrix ,处理以下类型的多个查询:
matrix 中单元格的值。(row1, col1) 和 右下角 (row2, col2) 定义的 matrix 内矩阵元素的 和。实现 NumMatrix 类:
NumMatrix(int[][] matrix) 用整数矩阵 matrix 初始化对象。void update(int row, int col, int val) 更新 matrix[row][col] 的值到 val 。int sumRegion(int row1, int col1, int row2, int col2) 返回矩阵 matrix 中指定矩形区域元素的 和 ,该区域由 左上角 (row1, col1) 和 右下角 (row2, col2) 界定。示例 1:
输入 ["NumMatrix", "sumRegion", "update", "sumRegion"] [[[[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]], [2, 1, 4, 3], [3, 2, 2], [2, 1, 4, 3]] 输出 [null, 8, null, 10]解释
NumMatrix numMatrix = new NumMatrix(3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5);
numMatrix.sumRegion(2, 1, 4, 3); // 返回 8 (即, 左侧红色矩形的和)
numMatrix.update(3, 2, 2); // 矩阵从左图变为右图
numMatrix.sumRegion(2, 1, 4, 3); // 返回 10 (即,右侧红色矩形的和)
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 200-105 <= matrix[i][j] <= 1050 <= row < m0 <= col < n-105 <= val <= 1050 <= row1 <= row2 < m0 <= col1 <= col2 < n104 次 sumRegion 和 update 方法方法一:树状数组
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 x 位置的数加上一个值 delta;query(x):查询序列 [1,...x] 区间的区间和,即位置 x 的前缀和。这两个操作的时间复杂度均为 。
对于本题,可以构建二维树状数组。
方法二:线段树
线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。
[1, N];[x, x];[l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r], 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。树状数组:
线段树:
树状数组:
class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } } class NumMatrix { private BinaryIndexedTree[] trees; public NumMatrix(int[][] matrix) { int m = matrix.length; int n = matrix[0].length; trees = new BinaryIndexedTree[m]; for (int i = 0; i < m; ++i) { BinaryIndexedTree tree = new BinaryIndexedTree(n); for (int j = 0; j < n; ++j) { tree.update(j + 1, matrix[i][j]); } trees[i] = tree; } } public void update(int row, int col, int val) { BinaryIndexedTree tree = trees[row]; int prev = tree.query(col + 1) - tree.query(col); tree.update(col + 1, val - prev); } public int sumRegion(int row1, int col1, int row2, int col2) { int s = 0; for (int i = row1; i <= row2; ++i) { BinaryIndexedTree tree = trees[i]; s += tree.query(col2 + 1) - tree.query(col1); } return s; } } /** * Your NumMatrix object will be instantiated and called as such: * NumMatrix obj = new NumMatrix(matrix); * obj.update(row,col,val); * int param_2 = obj.sumRegion(row1,col1,row2,col2); */
线段树:
class Node { int l; int r; int v; } class SegmentTree { private Node[] tr; private int[] nums; public SegmentTree(int[] nums) { int n = nums.length; tr = new Node[n << 2]; this.nums = nums; for (int i = 0; i < tr.length; ++i) { tr[i] = new Node(); } build(1, 1, n); } public void build(int u, int l, int r) { tr[u].l = l; tr[u].r = r; if (l == r) { tr[u].v = nums[l - 1]; return; } int mid = (l + r) >> 1; build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r); pushup(u); } public void modify(int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) { tr[u].v = v; return; } int mid = (tr[u].l + tr[u].r) >> 1; if (x <= mid) { modify(u << 1, x, v); } else { modify(u << 1 | 1, x, v); } pushup(u); } public void pushup(int u) { tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v; } public int query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) { return tr[u].v; } int mid = (tr[u].l + tr[u].r) >> 1; int v = 0; if (l <= mid) { v += query(u << 1, l, r); } if (r > mid) { v += query(u << 1 | 1, l, r); } return v; } } class NumMatrix { private SegmentTree[] trees; public NumMatrix(int[][] matrix) { int m = matrix.length; trees = new SegmentTree[m]; for (int i = 0; i < m; ++i) { trees[i] = new SegmentTree(matrix[i]); } } public void update(int row, int col, int val) { SegmentTree tree = trees[row]; tree.modify(1, col + 1, val); } public int sumRegion(int row1, int col1, int row2, int col2) { int s = 0; for (int row = row1; row <= row2; ++row) { SegmentTree tree = trees[row]; s += tree.query(1, col1 + 1, col2 + 1); } return s; } } /** * Your NumMatrix object will be instantiated and called as such: * NumMatrix obj = new NumMatrix(matrix); * obj.update(row,col,val); * int param_2 = obj.sumRegion(row1,col1,row2,col2); */
树状数组:
线段树:
树状数组:
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1] 输出: 0
提示:
1 <= prices.length <= 50000 <= prices[i] <= 1000动态规划法。
设 f1 表示当天买入股票后的最大利润,f2 表示当天卖出股票后的最大利润,f3 表示当天空仓后的最大利润。
初始第 1 天结束时,f1 = -prices[0],f2 = 0,f3 = 0。
从第 2 天开始,当天结束时:
f1 = max(f1, f3 - prices[i])。f2 = max(f2, f1 + prices[i])。f3 = max(f3, f2)。最后返回 f2 即可。
class Solution { public int maxProfit(int[] prices) { // 买入,卖出,继续空仓 int f1 = -prices[0], f2 = 0, f3 = 0; for (int i = 1; i < prices.length; ++i) { int pf1 = f1, pf2 = f2, pf3 = f3; f1 = Math.max(pf1, pf3 - prices[i]); f2 = Math.max(pf2, pf1 + prices[i]); f3 = Math.max(pf3, pf2); } return f2; } }
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。示例 1:
输入:n = 4, edges = [[1,0],[1,2],[1,3]] 输出:[1] 解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
示例 2:
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] 输出:[3,4]
提示:
1 <= n <= 2 * 104edges.length == n - 10 <= ai, bi < nai != bi(ai, bi) 互不相同拓扑排序,BFS 实现。
每一轮删除入度为 1 的节点,同时减小与之连接的节点的入度。循环此操作,最后一轮删除的节点,即为要找的最小高度树的根节点。
class Solution { public List<Integer> findMinHeightTrees(int n, int[][] edges) { if (n == 1) { return Collections.singletonList(0); } List<Integer>[] g = new List[n]; Arrays.setAll(g, k -> new ArrayList<>()); int[] degree = new int[n]; for (int[] e : edges) { int a = e[0], b = e[1]; g[a].add(b); g[b].add(a); ++degree[a]; ++degree[b]; } Queue<Integer> q = new LinkedList<>(); for (int i = 0; i < n; ++i) { if (degree[i] == 1) { q.offer(i); } } List<Integer> ans = new ArrayList<>(); while (!q.isEmpty()) { ans.clear(); for (int i = q.size(); i > 0; --i) { int a = q.poll(); ans.add(a); for (int b : g[a]) { if (--degree[b] == 1) { q.offer(b); } } } } return ans; } }
给定两个 稀疏矩阵 :大小为 m x k 的稀疏矩阵 mat1 和大小为 k x n 的稀疏矩阵 mat2 ,返回 mat1 x mat2 的结果。你可以假设乘法总是可能的。
示例 1:

输入:mat1 = [[1,0,0],[-1,0,3]], mat2 = [[7,0,0],[0,0,0],[0,0,1]] 输出:[[7,0,0],[-7,0,3]]
示例 2:
输入:mat1 = [[0]], mat2 = [[0]] 输出:[[0]]
提示:
m == mat1.lengthk == mat1[i].length == mat2.lengthn == mat2[i].length1 <= m, n, k <= 100-100 <= mat1[i][j], mat2[i][j] <= 100直接模拟。
用哈希表记录稀疏矩阵 mat1 中的非 0 值。
class Solution { public int[][] multiply(int[][] mat1, int[][] mat2) { int r1 = mat1.length, c1 = mat1[0].length, c2 = mat2[0].length; int[][] res = new int[r1][c2]; Map<Integer, List<Integer>> mp = new HashMap<>(); for (int i = 0; i < r1; ++i) { for (int j = 0; j < c1; ++j) { if (mat1[i][j] != 0) { mp.computeIfAbsent(i, k -> new ArrayList<>()).add(j); } } } for (int i = 0; i < r1; ++i) { for (int j = 0; j < c2; ++j) { if (mp.containsKey(i)) { for (int k : mp.get(i)) { res[i][j] += mat1[i][k] * mat2[k][j]; } } } } return res; } }
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
示例 1:
输入:nums = [3,1,5,8] 输出:167 解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
示例 2:
输入:nums = [1,5] 输出:10
提示:
n == nums.length1 <= n <= 3000 <= nums[i] <= 100区间 DP。
dp[i][j] 表示戳破区间 (i, j) 内所有气球获得的最大硬币数。(i, j) 中以气球 k 作为最后戳破的气球。那么 dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]), k ∈ [i + 1, j)。以区间长度 l 从小到大开始处理每个状态值。
class Solution { public int maxCoins(int[] nums) { int[] vals = new int[nums.length + 2]; vals[0] = 1; vals[vals.length - 1] = 1; System.arraycopy(nums, 0, vals, 1, nums.length); int n = vals.length; int[][] dp = new int[n][n]; for (int l = 2; l < n; ++l) { for (int i = 0; i + l < n; ++i) { int j = i + l; for (int k = i + 1; k < j; ++k) { dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + vals[i] * vals[k] * vals[j]); } } } return dp[0][n - 1]; } }
超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。
给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。
题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。
示例 1:
输入:n = 12, primes = [2,7,13,19] 输出:32 解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。
示例 2:
输入:n = 1, primes = [2,3,5] 输出:1 解释:1 不含质因数,因此它的所有质因数都在质数数组 primes = [2,3,5] 中。
提示:
1 <= n <= 1051 <= primes.length <= 1002 <= primes[i] <= 1000primes[i] 是一个质数primes 中的所有值都 互不相同 ,且按 递增顺序 排列方法一:优先队列(小根堆)
我们用一个优先队列(小根堆)维护所有可能的超级丑数,初始时将 放入队列中。
每次从队列中取出最小的超级丑数 ,将 乘以数组 primes 中的每个数,将乘积放入队列中,然后重复上述操作 次即可得到第 个超级丑数。
由于题目保证第 个超级丑数在 位带符号整数范围内,因此,我们将乘积放入队列之前,可以先判断乘积是否超过 ,如果超过,则不需要将乘积放入队列中。另外,可以使用欧拉筛优化。
时间复杂度 ,空间复杂度 。其中 和 分别为数组 primes 的长度和给定的整数 。
class Solution { public int nthSuperUglyNumber(int n, int[] primes) { PriorityQueue<Integer> q = new PriorityQueue<>(); q.offer(1); int x = 0; while (n-- > 0) { x = q.poll(); while (!q.isEmpty() && q.peek() == x) { q.poll(); } for (int k : primes) { if (k <= Integer.MAX_VALUE / x) { q.offer(k * x); } if (x % k == 0) { break; } } } return x; } }
给你一个二叉树的根结点,返回其结点按 垂直方向(从上到下,逐列)遍历的结果。
如果两个结点在同一行和列,那么顺序则为 从左到右。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[9],[3,15],[20],[7]]
示例 2:
输入:root = [3,9,8,4,0,1,7] 输出:[[4],[9],[3,0,1],[8],[7]]
示例 3:
输入:root = [3,9,8,4,0,1,7,null,null,null,2,5] 输出:[[4],[9,5],[3,0,1],[8,2],[7]]
提示:
[0, 100] 内-100 <= Node.val <= 100方法一:DFS
DFS 遍历二叉树,记录每个节点的值、深度,以及横向的偏移量。然后对所有节点按照横向偏移量从小到大排序,再按照深度从小到大排序,最后按照横向偏移量分组。
时间复杂度 ,空间复杂度 。其中 为二叉树的节点个数。
方法二:BFS
本题较好的做法应该是 BFS,从上往下逐层进行遍历。
时间复杂度 ,空间复杂度 。其中 是二叉树的结点数。
class Solution { private TreeMap<Integer, List<int[]>> d = new TreeMap<>(); public List<List<Integer>> verticalOrder(TreeNode root) { dfs(root, 0, 0); List<List<Integer>> ans = new ArrayList<>(); for (var v : d.values()) { Collections.sort(v, (a, b) -> a[0] - b[0]); List<Integer> t = new ArrayList<>(); for (var e : v) { t.add(e[1]); } ans.add(t); } return ans; } private void dfs(TreeNode root, int depth, int offset) { if (root == null) { return; } d.computeIfAbsent(offset, k -> new ArrayList<>()).add(new int[] {depth, root.val}); dfs(root.left, depth + 1, offset - 1); dfs(root.right, depth + 1, offset + 1); } }
class Solution { public List<List<Integer>> verticalOrder(TreeNode root) { List<List<Integer>> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<Pair<TreeNode, Integer>> q = new ArrayDeque<>(); q.offer(new Pair<>(root, 0)); TreeMap<Integer, List<Integer>> d = new TreeMap<>(); while (!q.isEmpty()) { for (int n = q.size(); n > 0; --n) { var p = q.pollFirst(); root = p.getKey(); int offset = p.getValue(); d.computeIfAbsent(offset, k -> new ArrayList()).add(root.val); if (root.left != null) { q.offer(new Pair<>(root.left, offset - 1)); } if (root.right != null) { q.offer(new Pair<>(root.right, offset + 1)); } } } return new ArrayList<>(d.values()); } }
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例 1:
输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
示例 2:
输入:nums = [-1] 输出:[0]
示例 3:
输入:nums = [-1,-1] 输出:[0,0]
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 104方法一:树状数组
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 x 位置的数加上一个值 delta;query(x):查询序列 [1,...x] 区间的区间和,即位置 x 的前缀和。这两个操作的时间复杂度均为 。
树状数组最基本的功能就是求比某点 x 小的点的个数(这里的比较是抽象的概念,可以是数的大小、坐标的大小、质量的大小等等)。
比如给定数组 a[5] = {2, 5, 3, 4, 1},求 b[i] = 位置 i 左边小于等于 a[i] 的数的个数。对于此例,b[5] = {0, 1, 1, 2, 0}。
解决方案是直接遍历数组,每个位置先求出 query(a[i]),然后再修改树状数组 update(a[i], 1) 即可。当数的范围比较大时,需要进行离散化,即先进行去重并排序,然后对每个数字进行编号。
方法二:线段树
线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。
[1, N];[x, x];[l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r], 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。方法三:归并排序
树状数组:
线段树:
树状数组:
class Solution { public List<Integer> countSmaller(int[] nums) { Set<Integer> s = new HashSet<>(); for (int v : nums) { s.add(v); } List<Integer> alls = new ArrayList<>(s); alls.sort(Comparator.comparingInt(a -> a)); int n = alls.size(); Map<Integer, Integer> m = new HashMap<>(n); for (int i = 0; i < n; ++i) { m.put(alls.get(i), i + 1); } BinaryIndexedTree tree = new BinaryIndexedTree(n); LinkedList<Integer> ans = new LinkedList<>(); for (int i = nums.length - 1; i >= 0; --i) { int x = m.get(nums[i]); tree.update(x, 1); ans.addFirst(tree.query(x - 1)); } return ans; } } class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } }
线段树:
class Solution { public List<Integer> countSmaller(int[] nums) { Set<Integer> s = new HashSet<>(); for (int v : nums) { s.add(v); } List<Integer> alls = new ArrayList<>(s); alls.sort(Comparator.comparingInt(a -> a)); int n = alls.size(); Map<Integer, Integer> m = new HashMap<>(n); for (int i = 0; i < n; ++i) { m.put(alls.get(i), i + 1); } SegmentTree tree = new SegmentTree(n); LinkedList<Integer> ans = new LinkedList<>(); for (int i = nums.length - 1; i >= 0; --i) { int x = m.get(nums[i]); tree.modify(1, x, 1); ans.addFirst(tree.query(1, 1, x - 1)); } return ans; } } class Node { int l; int r; int v; } class SegmentTree { private Node[] tr; public SegmentTree(int n) { tr = new Node[4 * n]; for (int i = 0; i < tr.length; ++i) { tr[i] = new Node(); } build(1, 1, n); } public void build(int u, int l, int r) { tr[u].l = l; tr[u].r = r; if (l == r) { return; } int mid = (l + r) >> 1; build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r); } public void modify(int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) { tr[u].v += v; return; } int mid = (tr[u].l + tr[u].r) >> 1; if (x <= mid) { modify(u << 1, x, v); } else { modify(u << 1 | 1, x, v); } pushup(u); } public void pushup(int u) { tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v; } public int query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) { return tr[u].v; } int mid = (tr[u].l + tr[u].r) >> 1; int v = 0; if (l <= mid) { v += query(u << 1, l, r); } if (r > mid) { v += query(u << 1 | 1, l, r); } return v; } }
树状数组:
线段树:
树状数组:
归并排序:
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:
输入:s = "bcabc"
输出:"abc"
示例 2:
输入:s = "cbacdcbc" 输出:"acdb"
提示:
1 <= s.length <= 104s 由小写英文字母组成注意:该题与 1081 https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters 相同
方法一:栈
我们用一个数组 last 记录每个字符最后一次出现的位置,用栈来保存结果字符串,用一个数组 vis 或者一个整型变量 mask 记录当前字符是否在栈中。
遍历字符串 ,对于每个字符 ,如果 不在栈中,我们就需要判断栈顶元素是否大于 ,如果大于 ,且栈顶元素在后面还会出现,我们就将栈顶元素弹出,将 压入栈中。
最后将栈中元素拼接成字符串作为结果返回。
时间复杂度 。其中 为字符串 的长度。
class Solution { public String removeDuplicateLetters(String s) { int n = s.length(); int[] last = new int[26]; for (int i = 0; i < n; ++i) { last[s.charAt(i) - 'a'] = i; } Deque<Character> stk = new ArrayDeque<>(); int mask = 0; for (int i = 0; i < n; ++i) { char c = s.charAt(i); if (((mask >> (c - 'a')) & 1) == 1) { continue; } while (!stk.isEmpty() && stk.peek() > c && last[stk.peek() - 'a'] > i) { mask ^= 1 << (stk.pop() - 'a'); } stk.push(c); mask |= 1 << (c - 'a'); } StringBuilder ans = new StringBuilder(); for (char c : stk) { ans.append(c); } return ans.reverse().toString(); } }
给你一个 m × n 的网格,值为 0 、 1 或 2 ,其中:
0 代表一块你可以自由通过的 空地 1 代表一个你不能通过的 建筑2 标记一个你不能通过的 障碍 你想要在一块空地上建造一所房子,在 最短的总旅行距离 内到达所有的建筑。你只能上下左右移动。
返回到该房子的 最短旅行距离 。如果根据上述规则无法建造这样的房子,则返回 -1 。
总旅行距离 是朋友们家到聚会地点的距离之和。
使用 曼哈顿距离 计算距离,其中距离 (p1, p2) = |p2.x - p1.x | + | p2.y - p1.y | 。
示例 1:

输入:grid = [[1,0,2,0,1],[0,0,0,0,0],[0,0,1,0,0]] 输出:7 解析:给定三个建筑物 (0,0)、(0,4) 和 (2,2) 以及一个位于 (0,2) 的障碍物。 由于总距离之和 3+3+1=7 最优,所以位置 (1,2) 是符合要求的最优地点。 故返回7。
示例 2:
输入: grid = [[1,0]] 输出: 1
示例 3:
输入: grid = [[1]] 输出: -1
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 50grid[i][j] 是 0, 1 或 2grid 中 至少 有 一幢 建筑BFS。
记 total 变量表示建筑物(grid[i][j] = 1)的个数,cnt[i][j] 表示空地 (i, j) 上能到达的建筑物数量;dist[i][j] 表示空地 (i, j) 到每个建筑物的距离之和。求解的是满足 cnt[i][j] == total 的空地距离和的最小值。
class Solution { public int shortestDistance(int[][] grid) { int m = grid.length; int n = grid[0].length; Deque<int[]> q = new LinkedList<>(); int total = 0; int[][] cnt = new int[m][n]; int[][] dist = new int[m][n]; int[] dirs = {-1, 0, 1, 0, -1}; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == 1) { ++total; q.offer(new int[] {i, j}); int d = 0; boolean[][] vis = new boolean[m][n]; while (!q.isEmpty()) { ++d; for (int k = q.size(); k > 0; --k) { int[] p = q.poll(); for (int l = 0; l < 4; ++l) { int x = p[0] + dirs[l]; int y = p[1] + dirs[l + 1]; if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 0 && !vis[x][y]) { ++cnt[x][y]; dist[x][y] += d; q.offer(new int[] {x, y}); vis[x][y] = true; } } } } } } } int ans = Integer.MAX_VALUE; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == 0 && cnt[i][j] == total) { ans = Math.min(ans, dist[i][j]); } } } return ans == Integer.MAX_VALUE ? -1 : ans; } }
给你一个字符串数组 words ,找出并返回 length(words[i]) * length(words[j]) 的最大值,并且这两个单词不含有公共字母。如果不存在这样的两个单词,返回 0 。
示例 1:
输入:words = ["abcw","baz","foo","bar","xtfn","abcdef"] 输出:16 解释:这两个单词为 "abcw", "xtfn"。
示例 2:
输入:words = ["a","ab","abc","d","cd","bcd","abcd"] 输出:4 解释:这两个单词为 "ab", "cd"。
示例 3:
输入:words = ["a","aa","aaa","aaaa"] 输出:0 解释:不存在这样的两个单词。
提示:
2 <= words.length <= 10001 <= words[i].length <= 1000words[i] 仅包含小写字母class Solution { public int maxProduct(String[] words) { int n = words.length; int[] masks = new int[n]; for (int i = 0; i < n; ++i) { for (char c : words[i].toCharArray()) { masks[i] |= (1 << (c - 'a')); } } int ans = 0; for (int i = 0; i < n - 1; ++i) { for (int j = i + 1; j < n; ++j) { if ((masks[i] & masks[j]) == 0) { ans = Math.max(ans, words[i].length() * words[j].length()); } } } return ans; } }
初始时有 n 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。
第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 i 轮,你每 i 个灯泡就切换第 i 个灯泡的开关。直到第 n 轮,你只需要切换最后一个灯泡的开关。
找出并返回 n 轮后有多少个亮着的灯泡。
示例 1:

输入:n = 3 输出:1 解释: 初始时, 灯泡状态 [关闭, 关闭, 关闭]. 第一轮后, 灯泡状态 [开启, 开启, 开启]. 第二轮后, 灯泡状态 [开启, 关闭, 开启]. 第三轮后, 灯泡状态 [开启, 关闭, 关闭]. 你应该返回 1,因为只有一个灯泡还亮着。
示例 2:
输入:n = 0 输出:0
示例 3:
输入:n = 1 输出:1
提示:
0 <= n <= 109单词的 广义缩写词 可以通过下述步骤构造:先取任意数量的 不重叠、不相邻 的子字符串,再用它们各自的长度进行替换。
"abcde" 可以缩写为:
"a3e"("bcd" 变为 "3" )"1bcd1"("a" 和 "e" 都变为 "1")"5" ("abcde" 变为 "5")"abcde" (没有子字符串被代替)"23"("ab" 变为 "2" ,"cde" 变为 "3" )是无效的,因为被选择的字符串是相邻的"22de" ("ab" 变为 "2" , "bc" 变为 "2") 是无效的,因为被选择的字符串是重叠的给你一个字符串 word ,返回 一个由 word 的所有可能 广义缩写词 组成的列表 。按 任意顺序 返回答案。
示例 1:
输入:word = "word" 输出:["4","3d","2r1","2rd","1o2","1o1d","1or1","1ord","w3","w2d","w1r1","w1rd","wo2","wo1d","wor1","word"]
示例 2:
输入:word = "a" 输出:["1","a"]
提示:
1 <= word.length <= 15word 仅由小写英文字母组成回溯法。
class Solution { private List<String> ans; public List<String> generateAbbreviations(String word) { ans = new ArrayList<>(); List<String> t = new ArrayList<>(); dfs(word, t); return ans; } private void dfs(String s, List<String> t) { if ("".equals(s)) { ans.add(String.join("", t)); return; } for (int i = 1; i < s.length() + 1; ++i) { t.add(i + ""); if (i < s.length()) { t.add(String.valueOf(s.charAt(i))); dfs(s.substring(i + 1), t); t.remove(t.size() - 1); } else { dfs(s.substring(i), t); } t.remove(t.size() - 1); } t.add(String.valueOf(s.charAt(0))); dfs(s.substring(1), t); t.remove(t.size() - 1); } }
给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。
求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。
说明: 请尽可能地优化你算法的时间和空间复杂度。
示例 1:
输入: nums1 = [3, 4, 6, 5] nums2 = [9, 1, 2, 5, 8, 3] k = 5 输出: [9, 8, 6, 5, 3]
示例 2:
输入: nums1 = [6, 7] nums2 = [6, 0, 4] k = 5 输出: [6, 7, 6, 0, 4]
示例 3:
输入: nums1 = [3, 9] nums2 = [8, 9] k = 3 输出: [9, 8, 9]
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3 输出:-1
示例 3:
输入:coins = [1], amount = 0 输出:0
提示:
1 <= coins.length <= 121 <= coins[i] <= 231 - 10 <= amount <= 104方法一:动态规划
类似完全背包的思路,硬币数量不限,求凑成总金额所需的最少的硬币个数。
定义 表示从前 种硬币选出总金额为 所需的最少硬币数。
那么有:
令 ,则有:
因此,我们可以得到状态转移方程:
时间复杂度 ,空间复杂度 。其中 和 分别为硬币数量和总金额。
动态规划——完全背包问题朴素做法:
动态规划——完全背包问题空间优化:
class Solution { public int coinChange(int[] coins, int amount) { int m = coins.length; int[][] dp = new int[m + 1][amount + 1]; for (int i = 0; i <= m; ++i) { Arrays.fill(dp[i], amount + 1); } dp[0][0] = 0; for (int i = 1; i <= m; ++i) { int v = coins[i - 1]; for (int j = 0; j <= amount; ++j) { dp[i][j] = dp[i - 1][j]; if (j >= v) { dp[i][j] = Math.min(dp[i][j], dp[i][j - v] + 1); } } } return dp[m][amount] > amount ? - 1 : dp[m][amount]; } }
class Solution { public int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, amount + 1); dp[0] = 0; for (int coin : coins) { for (int j = coin; j <= amount; j++) { dp[j] = Math.min(dp[j], dp[j - coin] + 1); } } return dp[amount] > amount ? -1 : dp[amount]; } }
你有一个包含 n 个节点的图。给定一个整数 n 和一个数组 edges ,其中 edges[i] = [ai, bi] 表示图中 ai 和 bi 之间有一条边。
返回 图中已连接分量的数目 。
示例 1:

输入: n = 5, edges = [[0, 1], [1, 2], [3, 4]] 输出: 2
示例 2:

输入: n = 5, edges = [[0,1], [1,2], [2,3], [3,4]] 输出: 1
提示:
1 <= n <= 20001 <= edges.length <= 5000edges[i].length == 20 <= ai <= bi < nai != biedges 中不会出现重复的边并查集。
模板 1——朴素并查集:
模板 2——维护 size 的并查集:
模板 3——维护到祖宗节点距离的并查集:
class Solution { private int[] p; public int countComponents(int n, int[][] edges) { p = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; } for (int[] e : edges) { int a = e[0], b = e[1]; p[find(a)] = find(b); } int ans = 0; for (int i = 0; i < n; ++i) { if (i == find(i)) { ++ans; } } return ans; } private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; } }
给你一个整数数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序。
你可以假设所有输入数组都可以得到满足题目要求的结果。
示例 1:
输入:nums = [1,5,1,1,6,4] 输出:[1,6,1,5,1,4] 解释:[1,4,1,5,1,6] 同样是符合题目要求的结果,可以被判题程序接受。
示例 2:
输入:nums = [1,3,2,2,3,1] 输出:[2,3,1,3,1,2]
提示:
1 <= nums.length <= 5 * 1040 <= nums[i] <= 5000nums ,总能产生满足题目要求的结果进阶:你能用 O(n) 时间复杂度和 / 或原地 O(1) 额外空间来实现吗?
方法一:排序
方法二:桶排序
class Solution { public void wiggleSort(int[] nums) { int[] arr = nums.clone(); Arrays.sort(arr); int n = nums.length; int i = (n - 1) >> 1, j = n - 1; for (int k = 0; k < n; ++k) { if (k % 2 == 0) { nums[k] = arr[i--]; } else { nums[k] = arr[j--]; } } } }
class Solution { public void wiggleSort(int[] nums) { int[] bucket = new int[5001]; for (int v : nums) { ++bucket[v]; } int n = nums.length; int j = 5000; for (int i = 1; i < n; i += 2) { while (bucket[j] == 0) { --j; } nums[i] = j; --bucket[j]; } for (int i = 0; i < n; i += 2) { while (bucket[j] == 0) { --j; } nums[i] = j; --bucket[j]; } } }
给定一个数组 nums 和一个目标值 k,找到和等于 k 的最长连续子数组长度。如果不存在任意一个符合要求的子数组,则返回 0。
示例 1:
输入: nums = [1,-1,5,-2,3], k = 3 输出: 4 解释: 子数组 [1, -1, 5, -2] 和等于 3,且长度最长。
示例 2:
输入: nums = [-2,-1,2,1], k = 1 输出: 2 解释: 子数组 [-1, 2] 和等于 1,且长度最长。
提示:
1 <= nums.length <= 2 * 105-104 <= nums[i] <= 104-109 <= k <= 109方法一:哈希表 + 前缀和
我们可以用一个哈希表 记录数组 中每个前缀和第一次出现的下标,初始时 。另外定义一个变量 记录前缀和。
接下来,遍历数组 ,对于当前遍历到的数字 ,我们更新前缀和 ,如果 在哈希表 中存在,不妨记 ,那么以 结尾的符合条件的子数组的长度为 ,我们使用一个变量 来维护最长的符合条件的子数组的长度。然后,如果 在哈希表中不存在,我们记录 和对应的下标 ,即 ,否则我们不更新 。需要注意的是,可能会有多个位置 都满足 的值,因此我们只记录最小的 ,这样就能保证子数组的长度最长。
遍历结束之后,我们返回 即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
class Solution { public int maxSubArrayLen(int[] nums, int k) { Map<Long, Integer> d = new HashMap<>(); d.put(0L, -1); int ans = 0; long s = 0; for (int i = 0; i < nums.length; ++i) { s += nums[i]; ans = Math.max(ans, i - d.getOrDefault(s - k, i)); d.putIfAbsent(s, i); } return ans; } }
给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。
整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3x
示例 1:
输入:n = 27 输出:true
示例 2:
输入:n = 0 输出:false
示例 3:
输入:n = 9 输出:true
示例 4:
输入:n = 45 输出:false
提示:
-231 <= n <= 231 - 1进阶:你能不使用循环或者递归来完成本题吗?
方法一:试除法
如果 ,我们可以不断地将 除以 ,如果不能整除,说明 不是 的幂,否则继续除以 ,直到 小于等于 。如果 等于 ,说明 是 的幂,否则不是 的幂。
时间复杂度 ,空间复杂度 。
方法二:数学
如果 是 的幂,那么 最大是 ,因此我们只需要判断 是否是 的约数即可。
时间复杂度 ,空间复杂度 。
class Solution { public boolean isPowerOfThree(int n) { while (n > 2) { if (n % 3 != 0) { return false; } n /= 3; } return n == 1; } }
class Solution { public boolean isPowerOfThree(int n) { return n > 0 && 1162261467 % n == 0; } }
给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
示例 1:
输入:nums = [-2,5,-1], lower = -2, upper = 2 输出:3 解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。
示例 2:
输入:nums = [0], lower = 0, upper = 0 输出:1
提示:
1 <= nums.length <= 105-231 <= nums[i] <= 231 - 1-105 <= lower <= upper <= 105方法一:前缀和 + 树状数组
题目要求区间和,因此我们可以先求出前缀和数组 ,其中 表示 中前 个元素的和。那么对于任意的 , 就是 中下标在 的元素之和。
而 ,可以转换为 ,也就是说,对于当前前缀和 ,我们需要统计 中有多少个下标 满足 。
我们可以用树状数组来维护每个前缀和出现的次数,这样对于每个前缀和 ,我们只需要查询树状数组中有多少个前缀和 满足 即可。
时间复杂度 ,空间复杂度 。其中 为数组长度。
class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; this.c = new int[n + 1]; } public void update(int x, int v) { while (x <= n) { c[x] += v; x += x & -x; } } public int query(int x) { int s = 0; while (x != 0) { s += c[x]; x -= x & -x; } return s; } } class Solution { public int countRangeSum(int[] nums, int lower, int upper) { int n = nums.length; long[] s = new long[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } long[] arr = new long[n * 3 + 3]; for (int i = 0, j = 0; i <= n; ++i, j += 3) { arr[j] = s[i]; arr[j + 1] = s[i] - lower; arr[j + 2] = s[i] - upper; } Arrays.sort(arr); int m = 0; for (int i = 0; i < arr.length; ++i) { if (i == 0 || arr[i] != arr[i - 1]) { arr[m++] = arr[i]; } } BinaryIndexedTree tree = new BinaryIndexedTree(m); int ans = 0; for (long x : s) { int l = search(arr, m, x - upper); int r = search(arr, m, x - lower); ans += tree.query(r) - tree.query(l - 1); tree.update(search(arr, m, x), 1); } return ans; } private int search(long[] nums, int r, long x) { int l = 0; while (l < r) { int mid = (l + r) >> 1; if (nums[mid] >= x) { r = mid; } else { l = mid + 1; } } return l + 1; } }
给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。
第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。
请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。
你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。
示例 1:

输入: head = [1,2,3,4,5] 输出: [1,3,5,2,4]
示例 2:

输入: head = [2,1,3,5,6,4,7] 输出: [2,3,6,7,1,5,4]
提示:
n == 链表中的节点数0 <= n <= 104-106 <= Node.val <= 106方法一:一次遍历
我们可以用两个指针 和 分别表示奇数节点和偶数节点的尾节点。初始时,指针 指向链表的头节点 ,指针 指向链表的第二个节点 。另外,我们用一个指针 指向偶数节点的头节点 ,即指针 的初始位置。
遍历链表,我们将指针 指向 的下一个节点,即 ,然后将指针 向后移动一位,即 ;将指针 指向 的下一个节点,即 ,然后将指针 向后移动一位,即 。继续遍历,直到 到达链表的末尾。
最后,我们将奇数节点的尾节点 指向偶数节点的头节点 ,即 ,然后返回链表的头节点 即可。
时间复杂度 ,其中 是链表的长度,需要遍历链表一次。空间复杂度 。只需要维护有限的指针。
class Solution { public ListNode oddEvenList(ListNode head) { if (head == null) { return null; } ListNode a = head; ListNode b = head.next, c = b; while (b != null && b.next != null) { a.next = b.next; a = a.next; b.next = a.next; b = b.next; } a.next = c; return head; } }
给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。
示例 1:
输入:matrix = [[9,9,4],[6,6,8],[2,1,1]] 输出:4 解释:最长递增路径为 [1, 2, 6, 9]。
示例 2:
输入:matrix = [[3,4,5],[3,2,6],[2,2,1]] 输出:4 解释:最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。
示例 3:
输入:matrix = [[1]] 输出:1
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 2000 <= matrix[i][j] <= 231 - 1方法一:记忆化搜索
我们设计一个函数 ,它表示从矩阵中的坐标 出发,可以得到的最长递增路径的长度。那么答案就是 。
函数 的执行逻辑如下:
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
相似题目:2328. 网格图中递增路径的数目。
class Solution { private int m; private int n; private int[][] matrix; private int[][] f; public int longestIncreasingPath(int[][] matrix) { m = matrix.length; n = matrix[0].length; f = new int[m][n]; this.matrix = matrix; int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0;j < n; ++j) { ans = Math.max(ans, dfs(i, j)); } } return ans; } private int dfs(int i, int j) { if (f[i][j] != 0) { return f[i][j]; } int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) { f[i][j] = Math.max(f[i][j], dfs(x, y)); } } return ++f[i][j]; } }
给定一个已排序的正整数数组 nums ,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。
请返回 满足上述要求的最少需要补充的数字个数 。
示例 1:
输入: nums = [1,3], n = 6 输出: 1 解释: 根据 nums 里现有的组合 [1], [3], [1,3],可以得出 1, 3, 4。 现在如果我们将 2 添加到 nums 中, 组合变为: [1], [2], [3], [1,3], [2,3], [1,2,3]。 其和可以表示数字 1, 2, 3, 4, 5, 6,能够覆盖 [1, 6] 区间里所有的数。 所以我们最少需要添加一个数字。
示例 2:
输入: nums = [1,5,10], n = 20 输出: 2 解释: 我们需要添加 [2,4]。
示例 3:
输入: nums = [1,2,2], n = 5 输出: 0
提示:
1 <= nums.length <= 10001 <= nums[i] <= 104nums 按 升序排列1 <= n <= 231 - 1方法一:贪心
我们假设数字 是最小的不能表示的正整数,那么 的这些数都是可以表示的。为了能表示数字 ,我们需要添加一个小于等于 的数:
因此,我们应该贪心地添加数字 ,这样可以覆盖的区间更大。
我们用一个变量 记录当前不能表示的最小正整数,初始化为 ,此时 是空的,表示当前没有任何数可以被覆盖;用一个变量 记录当前遍历到的数组下标。
循环进行以下操作:
最终答案即为补充的数的数量。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
class Solution { public int minPatches(int[] nums, int n) { long x = 1; int ans = 0; for (int i = 0; x <= n;) { if (i < nums.length && nums[i] <= x) { x += nums[i++]; } else { ++ans; x <<= 1; } } return ans; } }
序列化二叉树的一种方法是使用 前序遍历 。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。

例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
保证 每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。
你可以认为输入格式总是有效的
"1,,3" 。注意:不允许重建树。
示例 1:
输入: preorder = "9,3,4,#,#,1,#,#,2,#,6,#,#" 输出: true
示例 2:
输入: preorder = "1,#" 输出: false
示例 3:
输入: preorder = "9,#,#,1" 输出: false
提示:
1 <= preorder.length <= 104preorder 由以逗号 “,” 分隔的 [0,100] 范围内的整数和 “#” 组成给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
示例 1:
输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]] 输出:["JFK","MUC","LHR","SFO","SJC"]
示例 2:
输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] 输出:["JFK","ATL","JFK","SFO","ATL","SFO"] 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。
提示:
1 <= tickets.length <= 300tickets[i].length == 2fromi.length == 3toi.length == 3fromi 和 toi 由大写英文字母组成fromi != toi给定一个二叉树,找到其中最大的二叉搜索树(BST)子树,并返回该子树的大小。其中,最大指的是子树节点数最多的。
二叉搜索树(BST)中的所有节点都具备以下属性:
左子树的值小于其父(根)节点的值。
右子树的值大于其父(根)节点的值。
注意:子树必须包含其所有后代。
示例 1:

输入:root = [10,5,15,1,8,null,7] 输出:3 解释:本例中最大的 BST 子树是高亮显示的子树。返回值是子树的大小,即 3 。
示例 2:
输入:root = [4,2,7,2,3,5,null,2,null,null,null,null,null,1] 输出:2
提示:
[0, 104]-104 <= Node.val <= 104进阶: 你能想出 O(n) 时间复杂度的解法吗?
后序遍历,定义 dfs(root) 获取以当前结点为根结点的二叉搜索树的结点最小值、最大值、结点数。
class Solution { private int ans; public int largestBSTSubtree(TreeNode root) { ans = 0; dfs(root); return ans; } private int[] dfs(TreeNode root) { if (root == null) { return new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE, 0}; } int[] left = dfs(root.left); int[] right = dfs(root.right); if (left[1] < root.val && root.val < right[0]) { ans = Math.max(ans, left[2] + right[2] + 1); return new int[] { Math.min(root.val, left[0]), Math.max(root.val, right[1]), left[2] + right[2] + 1}; } return new int[] {Integer.MIN_VALUE, Integer.MAX_VALUE, 0}; } }
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4,5] 输出:true 解释:任何 i < j < k 的三元组都满足题意
示例 2:
输入:nums = [5,4,3,2,1] 输出:false 解释:不存在满足题意的三元组
示例 3:
输入:nums = [2,1,5,0,4,6] 输出:true 解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6
提示:
1 <= nums.length <= 5 * 105-231 <= nums[i] <= 231 - 1进阶:你能实现时间复杂度为 O(n) ,空间复杂度为 O(1) 的解决方案吗?
用 min, mid 记录遍历过程中遇到的最小值以及中间值,若出现 num > mid,说明找到了满足题目的三元组,返回 true;否则遍历结束返回 false。
class Solution { public boolean increasingTriplet(int[] nums) { int n = nums.length; int[] lmi = new int[n]; int[] rmx = new int[n]; lmi[0] = Integer.MAX_VALUE; rmx[n - 1] = Integer.MIN_VALUE; for (int i = 1; i < n; ++i) { lmi[i] = Math.min(lmi[i - 1], nums[i - 1]); } for (int i = n - 2; i >= 0; --i) { rmx[i] = Math.max(rmx[i + 1], nums[i + 1]); } for (int i = 0; i < n; ++i) { if (lmi[i] < nums[i] && nums[i] < rmx[i]) { return true; } } return false; } }
空间优化:
class Solution { public boolean increasingTriplet(int[] nums) { int min = Integer.MAX_VALUE, mid = Integer.MAX_VALUE; for (int num : nums) { if (num > mid) { return true; } if (num <= min) { min = num; } else { mid = num; } } return false; } }
给你一个整数数组 distance 。
从 X-Y 平面上的点 (0,0) 开始,先向北移动 distance[0] 米,然后向西移动 distance[1] 米,向南移动 distance[2] 米,向东移动 distance[3] 米,持续移动。也就是说,每次移动后你的方位会发生逆时针变化。
判断你所经过的路径是否相交。如果相交,返回 true ;否则,返回 false 。
示例 1:
输入:distance = [2,1,1,2] 输出:true
示例 2:
输入:distance = [1,2,3,4] 输出:false
示例 3:
输入:distance = [1,1,1,1] 输出:true
提示:
1 <= distance.length <= 1051 <= distance[i] <= 105 i-2
case 1 : i-1┌─┐
└─┼─>i
i-3
i-2
case 2 : i-1 ┌────┐
└─══>┘i-3
i i-4
case 3 : i-4
┌──┐
│i<┼─┐
i-3│ i-5│i-1
└────┘
i-2
class Solution { public boolean isSelfCrossing(int[] distance) { int[] d = distance; for (int i = 3; i < d.length; ++i) { if (d[i] >= d[i - 2] && d[i - 1] <= d[i - 3]) { return true; } if (i >= 4 && d[i - 1] == d[i - 3] && d[i] + d[i - 4] >= d[i - 2]) { return true; } if (i >= 5 && d[i - 2] >= d[i - 4] && d[i - 1] <= d[i - 3] && d[i] >= d[i - 2] - d[i - 4] && d[i - 1] + d[i - 5] >= d[i - 3]) { return true; } } return false; } }
给定一组 互不相同 的单词, 找出所有 不同 的索引对 (i, j),使得列表中的两个单词, words[i] + words[j] ,可拼接成回文串。
示例 1:
输入:words = ["abcd","dcba","lls","s","sssll"] 输出:[[0,1],[1,0],[3,2],[2,4]] 解释:可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]
示例 2:
输入:words = ["bat","tab","cat"] 输出:[[0,1],[1,0]] 解释:可拼接成的回文串为 ["battab","tabbat"]
示例 3:
输入:words = ["a",""] 输出:[[0,1],[1,0]]
提示:
1 <= words.length <= 50000 <= words[i].length <= 300words[i] 由小写英文字母组成方法一:字符串哈希
字符串哈希是把一个任意长度的字符串映射成一个非负整数,并且其冲突的概率几乎为 。字符串哈希用于计算字符串哈希值,快速判断两个字符串是否相等。
取一固定值 ,把字符串看作是 进制数,并分配一个大于 的数值,代表每种字符。一般来说,我们分配的数值都远小于 。例如,对于小写字母构成的字符串,可以令 , , ..., 。取一固定值 ,求出该 进制对 的余数,作为该字符串的 值。
一般来说,取 或者 ,此时 值产生的冲突概率极低。只要两个字符串 值相同,我们就认为两个字符串是相等的。通常 取 ,C++ 里,可以直接使用 unsigned long long 类型存储这个 值,在计算时不处理算术溢出问题,产生溢出时相当于自动对 取模,这样可以避免低效取模运算。
除了在极特殊构造的数据上,上述 算法很难产生冲突,一般情况下上述 算法完全可以出现在题目的标准答案中。我们还可以多取一些恰当的 和 的值(例如大质数),多进行几组 运算,当结果都相同时才认为原字符串相等,就更加难以构造出使这个 产生错误的数据。
方法二:前缀树
class Solution { private static final int BASE = 131; private static final long[] MUL = new long[310]; private static final int MOD = (int) 1e9 + 7; static { MUL[0] = 1; for (int i = 1; i < MUL.length; ++i) { MUL[i] = (MUL[i - 1] * BASE) % MOD; } } public List<List<Integer>> palindromePairs(String[] words) { int n = words.length; long[] prefix = new long[n]; long[] suffix = new long[n]; for (int i = 0; i < n; ++i) { String word = words[i]; int m = word.length(); for (int j = 0; j < m; ++j) { int t = word.charAt(j) - 'a' + 1; int s = word.charAt(m - j - 1) - 'a' + 1; prefix[i] = (prefix[i] * BASE) % MOD + t; suffix[i] = (suffix[i] * BASE) % MOD + s; } } List<List<Integer>> ans = new ArrayList<>(); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { if (check(i, j, words[j].length(), words[i].length(), prefix, suffix)) { ans.add(Arrays.asList(i, j)); } if (check(j, i, words[i].length(), words[j].length(), prefix, suffix)) { ans.add(Arrays.asList(j, i)); } } } return ans; } private boolean check(int i, int j, int n, int m, long[] prefix, long[] suffix) { long t = ((prefix[i] * MUL[n]) % MOD + prefix[j]) % MOD; long s = ((suffix[j] * MUL[m]) % MOD + suffix[i]) % MOD; return t == s; } }
class Trie { Trie[] children = new Trie[26]; Integer v; void insert(String w, int i) { Trie node = this; for (char c : w.toCharArray()) { c -= 'a'; if (node.children[c] == null) { node.children[c] = new Trie(); } node = node.children[c]; } node.v = i; } Integer search(String w, int i, int j) { Trie node = this; for (int k = j; k >= i; --k) { int idx = w.charAt(k) - 'a'; if (node.children[idx] == null) { return null; } node = node.children[idx]; } return node.v; } } class Solution { public List<List<Integer>> palindromePairs(String[] words) { Trie trie = new Trie(); int n = words.length; for (int i = 0; i < n; ++i) { trie.insert(words[i], i); } List<List<Integer>> ans = new ArrayList<>(); for (int i = 0; i < n; ++i) { String w = words[i]; int m = w.length(); for (int j = 0; j <= m; ++j) { if (isPalindrome(w, j, m - 1)) { Integer k = trie.search(w, 0, j - 1); if (k != null && k != i) { ans.add(Arrays.asList(i, k)); } } if (j != 0 && isPalindrome(w, 0, j - 1)) { Integer k = trie.search(w, j, m - 1); if (k != null && k != i) { ans.add(Arrays.asList(k, i)); } } } } return ans; } // TLE // private boolean isPalindrome(String w, int i, int j) { // for (; i < j; ++i, --j) { // if (w.charAt(i) != w.charAt(j)) { // return false; // } // } // return true; // } private boolean isPalindrome(String w, int start, int end) { int i = start, j = end; for (; i < j; ++i, --j) { if (w.charAt(i) != w.charAt(j)) { return false; } } return true; } }
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:

输入: root = [3,2,3,null,3,null,1] 输出: 7 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:

输入: root = [3,4,5,1,3,null,1] 输出: 9 解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
[1, 104] 范围内0 <= Node.val <= 104记忆化搜索。
class Solution { private Map<TreeNode, Integer> memo; public int rob(TreeNode root) { memo = new HashMap<>(); return dfs(root); } private int dfs(TreeNode root) { if (root == null) { return 0; } if (memo.containsKey(root)) { return memo.get(root); } int a = dfs(root.left) + dfs(root.right); int b = root.val; if (root.left != null) { b += dfs(root.left.left) + dfs(root.left.right); } if (root.right != null) { b += dfs(root.right.left) + dfs(root.right.right); } int res = Math.max(a, b); memo.put(root, res); return res; } }
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
示例 1:
输入:n = 2 输出:[0,1,1] 解释: 0 --> 0 1 --> 1 2 --> 10
示例 2:
输入:n = 5 输出:[0,1,1,2,1,2] 解释: 0 --> 0 1 --> 1 2 --> 10 3 --> 11 4 --> 100 5 --> 101
提示:
0 <= n <= 105进阶:
O(n log n) 的解决方案,你可以在线性时间复杂度 O(n) 内用一趟扫描解决此问题吗?__builtin_popcount )方法一:位运算
class Solution { public int[] countBits(int n) { int[] ans = new int[n + 1]; for (int i = 1; i <= n; ++i) { ans[i] = ans[i & (i - 1)] + 1; } return ans; } }
给定一个嵌套的整数列表 nestedList ,每个元素要么是整数,要么是列表。同时,列表中元素同样也可以是整数或者是另一个列表。
整数的 深度 是其在列表内部的嵌套层数。例如,嵌套列表 [1,[2,2],[[3],2],1] 中每个整数的值就是其深度。
请返回该列表按深度加权后所有整数的总和。
示例 1:

输入:nestedList = [[1,1],2,[1,1]] 输出:10 解释:因为列表中有四个深度为 2 的 1 ,和一个深度为 1 的 2。
示例 2:
输入:nestedList = [1,[4,[6]]] 输出:27 解释:一个深度为 1 的 1,一个深度为 2 的 4,一个深度为 3 的 6。所以,1 + 4*2 + 6*3 = 27。
示例 3:
输入:nestedList = [0] 输出:0
提示:
1 <= nestedList.length <= 50[-100, 100] 内50DFS 实现。
/** * // This is the interface that allows for creating nested lists. * // You should not implement it, or speculate about its implementation * public interface NestedInteger { * // Constructor initializes an empty nested list. * public NestedInteger(); * * // Constructor initializes a single integer. * public NestedInteger(int value); * * // @return true if this NestedInteger holds a single integer, rather than a nested list. * public boolean isInteger(); * * // @return the single integer that this NestedInteger holds, if it holds a single integer * // Return null if this NestedInteger holds a nested list * public Integer getInteger(); * * // Set this NestedInteger to hold a single integer. * public void setInteger(int value); * * // Set this NestedInteger to hold a nested list and adds a nested integer to it. * public void add(NestedInteger ni); * * // @return the nested list that this NestedInteger holds, if it holds a nested list * // Return empty list if this NestedInteger holds a single integer * public List<NestedInteger> getList(); * } */ class Solution { public int depthSum(List<NestedInteger> nestedList) { return dfs(nestedList, 1); } private int dfs(List<NestedInteger> nestedList, int depth) { int depthSum = 0; for (NestedInteger item : nestedList) { if (item.isInteger()) { depthSum += item.getInteger() * depth; } else { depthSum += dfs(item.getList(), depth + 1); } } return depthSum; } }
给你一个字符串 s 和一个整数 k ,请你找出 至多 包含 k 个 不同 字符的最长子串,并返回该子串的长度。
示例 1:
输入:s = "eceba", k = 2 输出:3 解释:满足题目要求的子串是 "ece" ,长度为 3 。
示例 2:
输入:s = "aa", k = 1 输出:2 解释:满足题目要求的子串是 "aa" ,长度为 2 。
提示:
1 <= s.length <= 5 * 1040 <= k <= 50方法一:滑动窗口 + 哈希表
我们可以使用滑动窗口的思想,维护一个滑动窗口,使得窗口内的字符串中不同字符的个数不超过 个。窗口内不同字符个数的统计可以用哈希表 cnt 来维护。
我们使用两个指针 和 分别表示滑动窗口的左右边界。我们先移动右边界 ,将字符 加入到窗口内,扩大滑动窗口,若此时窗口内不同字符的个数超过 个,则移动左边界 ,缩小滑动窗口,直到窗口内不同字符的个数不超过 个。此时我们可以更新答案的最大值,即 。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
class Solution { public int lengthOfLongestSubstringKDistinct(String s, int k) { Map<Character, Integer> cnt = new HashMap<>(); int n = s.length(); int ans = 0, j = 0; for (int i = 0; i < n; ++i) { char c = s.charAt(i); cnt.put(c, cnt.getOrDefault(c, 0) + 1); while (cnt.size() > k) { char t = s.charAt(j); cnt.put(t, cnt.getOrDefault(t, 0) - 1); if (cnt.get(t) == 0) { cnt.remove(t); } ++j; } ans = Math.max(ans, i - j + 1); } return ans; } }
给你一个嵌套的整数列表 nestedList 。每个元素要么是一个整数,要么是一个列表;该列表的元素也可能是整数或者是其他列表。请你实现一个迭代器将其扁平化,使之能够遍历这个列表中的所有整数。
实现扁平迭代器类 NestedIterator :
NestedIterator(List<NestedInteger> nestedList) 用嵌套列表 nestedList 初始化迭代器。int next() 返回嵌套列表的下一个整数。boolean hasNext() 如果仍然存在待迭代的整数,返回 true ;否则,返回 false 。你的代码将会用下述伪代码检测:
initialize iterator with nestedList
res = []
while iterator.hasNext()
append iterator.next() to the end of res
return res
如果 res 与预期的扁平化列表匹配,那么你的代码将会被判为正确。
示例 1:
输入:nestedList = [[1,1],2,[1,1]] 输出:[1,1,2,1,1] 解释:通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,1,2,1,1]。
示例 2:
输入:nestedList = [1,[4,[6]]] 输出:[1,4,6] 解释:通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,4,6]。
提示:
1 <= nestedList.length <= 500[-106, 106] 内方法一:递归
根据题意要求可以将 NestedInteger 数据结构视作一个 N 叉树,当元素为一个整数时,该节点是 N 叉树的叶子节点,当元素为一个整数数组时,该节点是 N 叉树的非叶子节点,数组中的每一个元素包含子树的所有节点。故直接递归遍历 N 叉树并记录所有的叶子节点即可。
方法二:直接展开
调用 hasNext 时,如果 nestedList 的第一个元素是列表类型,则不断展开这个元素,直到第一个元素是整数类型。 调用 Next 方法时,由于 hasNext() 方法已确保 nestedList 第一个元素为整数类型,直接返回即可。
/** * // This is the interface that allows for creating nested lists. * // You should not implement it, or speculate about its implementation * public interface NestedInteger { * * // @return true if this NestedInteger holds a single integer, rather than a nested list. * public boolean isInteger(); * * // @return the single integer that this NestedInteger holds, if it holds a single integer * // Return null if this NestedInteger holds a nested list * public Integer getInteger(); * * // @return the nested list that this NestedInteger holds, if it holds a nested list * // Return null if this NestedInteger holds a single integer * public List<NestedInteger> getList(); * } */ public class NestedIterator implements Iterator<Integer> { private List<Integer> vals; private Iterator<Integer> cur; public NestedIterator(List<NestedInteger> nestedList) { vals = new ArrayList<>(); dfs(nestedList); cur = vals.iterator(); } @Override public Integer next() { return cur.next(); } @Override public boolean hasNext() { return cur.hasNext(); } private void dfs(List<NestedInteger> nestedList) { for (NestedInteger e : nestedList) { if (e.isInteger()) { vals.add(e.getInteger()); } else { dfs(e.getList()); } } } } /** * Your NestedIterator object will be instantiated and called as such: * NestedIterator i = new NestedIterator(nestedList); * while (i.hasNext()) v[f()] = i.next(); */
递归:
直接展开:
给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。
整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == 4x
示例 1:
输入:n = 16 输出:true
示例 2:
输入:n = 5 输出:false
示例 3:
输入:n = 1 输出:true
提示:
-231 <= n <= 231 - 1进阶:你能不使用循环或者递归来完成本题吗?
class Solution { public boolean isPowerOfFour(int n) { return n > 0 && (n & (n - 1)) == 0 && (n & 0xaaaaaaaa) == 0; } }
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58方法一:动态规划
我们定义 表示正整数 能获得的最大乘积,初始化 。答案即为 。
状态转移方程为:
时间复杂度 ,空间复杂度 。其中 为正整数 。
方法二:数学
当 时, 不能拆分成至少两个正整数的和,因此 是最大乘积。当 时,我们尽可能多地拆分 ,当剩下的最后一段为 时,我们将其拆分为 ,这样乘积最大。
时间复杂度 ,空间复杂度 。
class Solution { public int integerBreak(int n) { int[] dp = new int[n + 1]; dp[1] = 1; for (int i = 2; i <= n; ++i) { for (int j = 1; j < i; ++j) { dp[i] = Math.max(Math.max(dp[i], dp[i - j] * j), (i - j) * j); } } return dp[n]; } }
class Solution { public int integerBreak(int n) { if (n < 4) { return n - 1; } if (n % 3 == 0) { return (int) Math.pow(3, n / 3); } if (n % 3 == 1) { return (int) Math.pow(3, n / 3 - 1) * 4; } return (int) Math.pow(3, n / 3) * 2; } }
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 105s[i] 都是 ASCII 码表中的可打印字符class Solution { public void reverseString(char[] s) { for (int i = 0, j = s.length - 1; i < j; ++i, --j) { char t = s[i]; s[i] = s[j]; s[j] = t; } } }
给你一个字符串 s ,仅反转字符串中的所有元音字母,并返回结果字符串。
元音字母包括 'a'、'e'、'i'、'o'、'u',且可能以大小写两种形式出现不止一次。
示例 1:
输入:s = "hello" 输出:"holle"
示例 2:
输入:s = "leetcode" 输出:"leotcede"
提示:
1 <= s.length <= 3 * 105s 由 可打印的 ASCII 字符组成将字符串转为字符数组(或列表),定义双指针 i、j,分别指向数组(列表)头部和尾部,当 i、j 指向的字符均为元音字母时,进行交换。
依次遍历,当 i >= j 时,遍历结束。将字符数组(列表)转为字符串返回即可。
class Solution { public String reverseVowels(String s) { Set<Character> vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); int i = 0, j = s.length() - 1; char[] chars = s.toCharArray(); while (i < j) { if (!vowels.contains(chars[i])) { ++i; continue; } if (!vowels.contains(chars[j])) { --j; continue; } char t = chars[i]; chars[i] = chars[j]; chars[j] = t; ++i; --j; } return new String(chars); } }
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算其所有整数的移动平均值。
实现 MovingAverage 类:
MovingAverage(int size) 用窗口大小 size 初始化对象。double next(int val) 计算并返回数据流中最后 size 个值的移动平均值。示例:
输入: ["MovingAverage", "next", "next", "next", "next"] [[3], [1], [10], [3], [5]] 输出: [null, 1.0, 5.5, 4.66667, 6.0] 解释: MovingAverage movingAverage = new MovingAverage(3); movingAverage.next(1); // 返回 1.0 = 1 / 1 movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2 movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3 movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3
提示:
1 <= size <= 1000-105 <= val <= 105next 方法 104 次方法一:循环数组
方法二:队列
class MovingAverage { private int[] arr; private int s; private int cnt; public MovingAverage(int size) { arr = new int[size]; } public double next(int val) { int idx = cnt % arr.length; s += val - arr[idx]; arr[idx] = val; ++cnt; return s * 1.0 / Math.min(cnt, arr.length); } } /** * Your MovingAverage object will be instantiated and called as such: * MovingAverage obj = new MovingAverage(size); * double param_1 = obj.next(val); */
class MovingAverage { private Deque<Integer> q = new ArrayDeque<>(); private int n; private int s; public MovingAverage(int size) { n = size; } public double next(int val) { if (q.size() == n) { s -= q.pollFirst(); } q.offer(val); s += val; return s * 1.0 / q.size(); } } /** * Your MovingAverage object will be instantiated and called as such: * MovingAverage obj = new MovingAverage(size); * double param_1 = obj.next(val); */
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
示例 2:
输入: nums = [1], k = 1 输出: [1]
提示:
1 <= nums.length <= 105k 的取值范围是 [1, 数组中不相同的元素的个数]k 个高频元素的集合是唯一的进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
方法一:哈希表 + 优先队列(小根堆)
使用哈希表统计每个元素出现的次数,然后使用优先队列(小根堆)维护前 个出现次数最多的元素。
时间复杂度 。
class Solution { public int[] topKFrequent(int[] nums, int k) { Map<Integer, Long> frequency = Arrays.stream(nums).boxed().collect( Collectors.groupingBy(Function.identity(), Collectors.counting())); Queue<Map.Entry<Integer, Long>> queue = new PriorityQueue<>(Map.Entry.comparingByValue()); for (var entry : frequency.entrySet()) { queue.offer(entry); if (queue.size() > k) { queue.poll(); } } return queue.stream().mapToInt(Map.Entry::getKey).toArray(); } }
class Solution { public int[] topKFrequent(int[] nums, int k) { Map<Integer, Integer> cnt = new HashMap<>(); for (int v : nums) { cnt.put(v, cnt.getOrDefault(v, 0) + 1); } PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]); for (var e : cnt.entrySet()) { pq.offer(new int[] {e.getKey(), e.getValue()}); if (pq.size() > k) { pq.poll(); } } int[] ans = new int[k]; for (int i = 0; i < k; ++i) { ans[i] = pq.poll()[0]; } return ans; } }
请在 n × n 的棋盘上,实现一个判定井字棋(Tic-Tac-Toe)胜负的神器,判断每一次玩家落子后,是否有胜出的玩家。
在这个井字棋游戏中,会有 2 名玩家,他们将轮流在棋盘上放置自己的棋子。
在实现这个判定器的过程中,你可以假设以下这些规则一定成立:
1. 每一步棋都是在棋盘内的,并且只能被放置在一个空的格子里;
2. 一旦游戏中有一名玩家胜出的话,游戏将不能再继续;
3. 一个玩家如果在同一行、同一列或者同一斜对角线上都放置了自己的棋子,那么他便获得胜利。
示例:
给定棋盘边长 n = 3, 玩家 1 的棋子符号是 "X",玩家 2 的棋子符号是 "O"。 TicTacToe toe = new TicTacToe(3); toe.move(0, 0, 1); -> 函数返回 0 (此时,暂时没有玩家赢得这场对决) |X| | | | | | | // 玩家 1 在 (0, 0) 落子。 | | | | toe.move(0, 2, 2); -> 函数返回 0 (暂时没有玩家赢得本场比赛) |X| |O| | | | | // 玩家 2 在 (0, 2) 落子。 | | | | toe.move(2, 2, 1); -> 函数返回 0 (暂时没有玩家赢得比赛) |X| |O| | | | | // 玩家 1 在 (2, 2) 落子。 | | |X| toe.move(1, 1, 2); -> 函数返回 0 (暂没有玩家赢得比赛) |X| |O| | |O| | // 玩家 2 在 (1, 1) 落子。 | | |X| toe.move(2, 0, 1); -> 函数返回 0 (暂无玩家赢得比赛) |X| |O| | |O| | // 玩家 1 在 (2, 0) 落子。 |X| |X| toe.move(1, 0, 2); -> 函数返回 0 (没有玩家赢得比赛) |X| |O| |O|O| | // 玩家 2 在 (1, 0) 落子. |X| |X| toe.move(2, 1, 1); -> 函数返回 1 (此时,玩家 1 赢得了该场比赛) |X| |O| |O|O| | // 玩家 1 在 (2, 1) 落子。 |X|X|X|
进阶:
您有没有可能将每一步的 move() 操作优化到比 O(n2) 更快吗?
思路同1275. 找出井字棋的获胜者。
class TicTacToe { private int n; private int[][] counter; /** Initialize your data structure here. */ public TicTacToe(int n) { counter = new int[2][(n << 1) + 2]; this.n = n; } /** Player {player} makes a move at ({row}, {col}). @param row The row of the board. @param col The column of the board. @param player The player, can be either 1 or 2. @return The current winning condition, can be either: 0: No one wins. 1: Player 1 wins. 2: Player 2 wins. */ public int move(int row, int col, int player) { counter[player - 1][row] += 1; counter[player - 1][col + n] += 1; if (row == col) { counter[player - 1][n << 1] += 1; } if (row + col == n - 1) { counter[player - 1][(n << 1) + 1] += 1; } if (counter[player - 1][row] == n || counter[player - 1][col + n] == n || counter[player - 1][n << 1] == n || counter[player - 1][(n << 1) + 1] == n) { return player; } return 0; } } /** * Your TicTacToe object will be instantiated and called as such: * TicTacToe obj = new TicTacToe(n); * int param_1 = obj.move(row,col,player); */
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 1000“哈希表”实现。
class Solution { public int[] intersection(int[] nums1, int[] nums2) { Set<Integer> s = new HashSet<>(); for (int num : nums1) { s.add(num); } Set<Integer> t = new HashSet<>(); for (int num : nums2) { if (s.contains(num)) { t.add(num); } } int[] res = new int[t.size()]; int i = 0; for (int num : t) { res[i++] = num; } return res; } }
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[4,9]
提示:
1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 1000进阶:
nums1 的大小比 nums2 小,哪种方法更优?nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?“哈希表”实现。
class Solution { public int[] intersect(int[] nums1, int[] nums2) { Map<Integer, Integer> counter = new HashMap<>(); for (int num : nums1) { counter.put(num, counter.getOrDefault(num, 0) + 1); } List<Integer> t = new ArrayList<>(); for (int num : nums2) { if (counter.getOrDefault(num, 0) > 0) { t.add(num); counter.put(num, counter.get(num) - 1); } } int[] res = new int[t.size()]; for (int i = 0; i < res.length; ++i) { res[i] = t.get(i); } return res; } }
我们都知道安卓有个手势解锁的界面,是一个 3 x 3 的点所绘制出来的网格。用户可以设置一个 “解锁模式” ,通过连接特定序列中的点,形成一系列彼此连接的线段,每个线段的端点都是序列中两个连续的点。如果满足以下两个条件,则 k 点序列是有效的解锁模式:
5 或 6 没有提前出现的情况下连接点 2 和 9 是有效的,因为从点 2 到点 9 的线没有穿过点 5 或 6 的中心。2 没有提前出现的情况下连接点 1 和 3 是无效的,因为从圆点 1 到圆点 3 的直线穿过圆点 2 的中心。以下是一些有效和无效解锁模式的示例:

[4,1,3,6] ,连接点 1 和点 3 时经过了未被连接过的 2 号点。[4,1,9,2] ,连接点 1 和点 9 时经过了未被连接过的 5 号点。[2,4,1,3,6] ,连接点 1 和点 3 是有效的,因为虽然它经过了点 2 ,但是点 2 在该手势中之前已经被连过了。[6,5,4,1,9,2] ,连接点 1 和点 9 是有效的,因为虽然它经过了按键 5 ,但是点 5 在该手势中之前已经被连过了。给你两个整数,分别为 m 和 n ,那么请返回有多少种 不同且有效的解锁模式 ,是 至少 需要经过 m 个点,但是 不超过 n 个点的。
两个解锁模式 不同 需满足:经过的点不同或者经过点的顺序不同。
示例 1:
输入:m = 1, n = 1 输出:9
示例 2:
输入:m = 1, n = 2 输出:65
提示:
1 <= m, n <= 9 给你一个由非负整数 a1, a2, ..., an 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。
实现 SummaryRanges 类:
SummaryRanges() 使用一个空数据流初始化对象。void addNum(int val) 向数据流中加入整数 val 。int[][] getIntervals() 以不相交区间 [starti, endi] 的列表形式返回对数据流中整数的总结。示例:
输入: ["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"] [[], [1], [], [3], [], [7], [], [2], [], [6], []] 输出: [null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]] 解释: SummaryRanges summaryRanges = new SummaryRanges(); summaryRanges.addNum(1); // arr = [1] summaryRanges.getIntervals(); // 返回 [[1, 1]] summaryRanges.addNum(3); // arr = [1, 3] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]] summaryRanges.addNum(7); // arr = [1, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]] summaryRanges.addNum(2); // arr = [1, 2, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]] summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]
提示:
0 <= val <= 104addNum 和 getIntervals 方法 3 * 104 次进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?
class SummaryRanges { private TreeMap<Integer, int[]> mp; public SummaryRanges() { mp = new TreeMap<>(); } public void addNum(int val) { Integer l = mp.floorKey(val); Integer r = mp.ceilingKey(val); if (l != null && r != null && mp.get(l)[1] + 1 == val && mp.get(r)[0] - 1 == val) { mp.get(l)[1] = mp.get(r)[1]; mp.remove(r); } else if (l != null && val <= mp.get(l)[1] + 1) { mp.get(l)[1] = Math.max(val, mp.get(l)[1]); } else if (r != null && val >= mp.get(r)[0] - 1) { mp.get(r)[0] = Math.min(val, mp.get(r)[0]); } else { mp.put(val, new int[] {val, val}); } } public int[][] getIntervals() { int[][] res = new int[mp.size()][2]; int i = 0; for (int[] range : mp.values()) { res[i++] = range; } return res; } } /** * Your SummaryRanges object will be instantiated and called as such: * SummaryRanges obj = new SummaryRanges(); * obj.addNum(val); * int[][] param_2 = obj.getIntervals(); */
请你设计一个 贪吃蛇游戏,该游戏将会在一个 屏幕尺寸 = 宽度 x 高度 的屏幕上运行。如果你不熟悉这个游戏,可以 点击这里 在线试玩。
起初时,蛇在左上角的 (0, 0) 位置,身体长度为 1 个单位。
你将会被给出一个数组形式的食物位置序列 food ,其中 food[i] = (ri, ci) 。当蛇吃到食物时,身子的长度会增加 1 个单位,得分也会 +1 。
食物不会同时出现,会按列表的顺序逐一显示在屏幕上。比方讲,第一个食物被蛇吃掉后,第二个食物才会出现。
当一个食物在屏幕上出现时,保证 不会 出现在被蛇身体占据的格子里。
如果蛇越界(与边界相撞)或者头与 移动后 的身体相撞(即,身长为 4 的蛇无法与自己相撞),游戏结束。
实现 SnakeGame 类:
SnakeGame(int width, int height, int[][] food) 初始化对象,屏幕大小为 height x width ,食物位置序列为 foodint move(String direction) 返回蛇在方向 direction 上移动后的得分。如果游戏结束,返回 -1 。示例 1:
输入: ["SnakeGame", "move", "move", "move", "move", "move", "move"] [[3, 2, [[1, 2], [0, 1]]], ["R"], ["D"], ["R"], ["U"], ["L"], ["U"]] 输出: [null, 0, 0, 1, 1, 2, -1]解释:
SnakeGame snakeGame = new SnakeGame(3, 2, 1, 2], [0, 1);
snakeGame.move("R"); // 返回 0
snakeGame.move("D"); // 返回 0
snakeGame.move("R"); // 返回 1 ,蛇吃掉了第一个食物,同时第二个食物出现在 (0, 1)
snakeGame.move("U"); // 返回 1
snakeGame.move("L"); // 返回 2 ,蛇吃掉了第二个食物,没有出现更多食物
snakeGame.move("U"); // 返回 -1 ,蛇与边界相撞,游戏结束
提示:
1 <= width, height <= 1041 <= food.length <= 50food[i].length == 20 <= ri < height0 <= ci < widthdirection.length == 1direction is 'U', 'D', 'L', or 'R'.104 次 move 方法给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
示例 1:
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]] 输出:3 解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
示例 2:
输入:envelopes = [[1,1],[1,1],[1,1]] 输出:1
提示:
1 <= envelopes.length <= 105envelopes[i].length == 21 <= wi, hi <= 105按 w 进行升序排序,若 w 相同则按 h 降序排序。然后问题转换为求 h 数组的最长递增子序列长度。参考 300. 最长递增子序列。
方法一:贪心 + 二分查找
时间复杂度 O(nlogn)。
class Solution { public int maxEnvelopes(int[][] envelopes) { Arrays.sort(envelopes, (a, b) -> { return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]; }); int n = envelopes.length; int[] d = new int[n + 1]; d[1] = envelopes[0][1]; int size = 1; for (int i = 1; i < n; ++i) { int x = envelopes[i][1]; if (x > d[size]) { d[++size] = x; } else { int left = 1, right = size; while (left < right) { int mid = (left + right) >> 1; if (d[mid] >= x) { right = mid; } else { left = mid + 1; } } int p = d[left] >= x ? left : 1; d[p] = x; } } return size; } }
设计一个简化版的推特(Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近 10 条推文。
实现 Twitter 类:
Twitter() 初始化简易版推特对象void postTweet(int userId, int tweetId) 根据给定的 tweetId 和 userId 创建一条新推文。每次调用此函数都会使用一个不同的 tweetId 。List<Integer> getNewsFeed(int userId) 检索当前用户新闻推送中最近 10 条推文的 ID 。新闻推送中的每一项都必须是由用户关注的人或者是用户自己发布的推文。推文必须 按照时间顺序由最近到最远排序 。void follow(int followerId, int followeeId) ID 为 followerId 的用户开始关注 ID 为 followeeId 的用户。void unfollow(int followerId, int followeeId) ID 为 followerId 的用户不再关注 ID 为 followeeId 的用户。示例:
输入 ["Twitter", "postTweet", "getNewsFeed", "follow", "postTweet", "getNewsFeed", "unfollow", "getNewsFeed"] [[], [1, 5], [1], [1, 2], [2, 6], [1], [1, 2], [1]] 输出 [null, null, [5], null, null, [6, 5], null, [5]] 解释 Twitter twitter = new Twitter(); twitter.postTweet(1, 5); // 用户 1 发送了一条新推文 (用户 id = 1, 推文 id = 5) twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含一个 id 为 5 的推文 twitter.follow(1, 2); // 用户 1 关注了用户 2 twitter.postTweet(2, 6); // 用户 2 发送了一个新推文 (推文 id = 6) twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含两个推文,id 分别为 -> [6, 5] 。推文 id 6 应当在推文 id 5 之前,因为它是在 5 之后发送的 twitter.unfollow(1, 2); // 用户 1 取消关注了用户 2 twitter.getNewsFeed(1); // 用户 1 获取推文应当返回一个列表,其中包含一个 id 为 5 的推文。因为用户 1 已经不再关注用户 2
提示:
1 <= userId, followerId, followeeId <= 5000 <= tweetId <= 104postTweet、getNewsFeed、follow 和 unfollow 方法最多调用 3 * 104 次“哈希表 + 堆”实现。
class Twitter { private Map<Integer, List<Integer>> userTweets; private Map<Integer, Set<Integer>> userFollowing; private Map<Integer, Integer> tweets; private int time; /** Initialize your data structure here. */ public Twitter() { userTweets = new HashMap<>(); userFollowing = new HashMap<>(); tweets = new HashMap<>(); time = 0; } /** Compose a new tweet. */ public void postTweet(int userId, int tweetId) { userTweets.computeIfAbsent(userId, k -> new ArrayList<>()).add(tweetId); tweets.put(tweetId, ++time); } /** * Retrieve the 10 most recent tweet ids in the user's news feed. Each item in the news feed * must be posted by users who the user followed or by the user herself. Tweets must be ordered * from most recent to least recent. */ public List<Integer> getNewsFeed(int userId) { Set<Integer> following = userFollowing.getOrDefault(userId, new HashSet<>()); Set<Integer> users = new HashSet<>(following); users.add(userId); PriorityQueue<Integer> pq = new PriorityQueue<>(10, (a, b) -> (tweets.get(b) - tweets.get(a))); for (Integer u : users) { List<Integer> userTweet = userTweets.get(u); if (userTweet != null && !userTweet.isEmpty()) { for (int i = userTweet.size() - 1, k = 10; i >= 0 && k > 0; --i, --k) { pq.offer(userTweet.get(i)); } } } List<Integer> res = new ArrayList<>(); while (!pq.isEmpty() && res.size() < 10) { res.add(pq.poll()); } return res; } /** Follower follows a followee. If the operation is invalid, it should be a no-op. */ public void follow(int followerId, int followeeId) { userFollowing.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId); } /** Follower unfollows a followee. If the operation is invalid, it should be a no-op. */ public void unfollow(int followerId, int followeeId) { userFollowing.computeIfAbsent(followerId, k -> new HashSet<>()).remove(followeeId); } } /** * Your Twitter object will be instantiated and called as such: * Twitter obj = new Twitter(); * obj.postTweet(userId,tweetId); * List<Integer> param_2 = obj.getNewsFeed(userId); * obj.follow(followerId,followeeId); * obj.unfollow(followerId,followeeId); */
在一个二维平面空间中,给你 n 个点的坐标。问,是否能找出一条平行于 y 轴的直线,让这些点关于这条直线成镜像排布?
注意:题目数据中可能有重复的点。
示例 1:
输入:points = [[1,1],[-1,1]] 输出:true 解释:可以找出 x = 0 这条线。
示例 2:
输入:points = [[1,1],[-1,-1]] 输出:false 解释:无法找出这样一条线。
提示:
n == points.length1 <= n <= 10^4-10^8 <= points[i][j] <= 10^8进阶:你能找到比 O(n2) 更优的解法吗?
先找出所有点中的最小、最大的 x 坐标 minX 和 maxX。若存在满足条件的直线,则直线 x = (minX + maxX) / 2。(或者说:s = minX + maxX)
遍历每个点 point(x, y),若 (s - x, y) 不在点集里,说明不满足条件,直接返回 false。遍历结束返回 true。
class Solution { public boolean isReflected(int[][] points) { int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE; Set<String> pointSet = new HashSet<>(); for (int[] point : points) { minX = Math.min(minX, point[0]); maxX = Math.max(maxX, point[0]); pointSet.add(point[0] + "." + point[1]); } long s = minX + maxX; for (int[] point : points) { if (!pointSet.contains((s - point[0]) + "." + point[1])) { return false; } } return true; } }
给你一个整数 n ,统计并返回各位数字都不同的数字 x 的个数,其中 0 <= x < 10n 。
示例 1:
输入:n = 2 输出:91 解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。
示例 2:
输入:n = 0 输出:1
提示:
0 <= n <= 8方法一:排列组合
当 时,有 ,只有 个数字,即 。
当 时,有 ,有 个数字,即 。
当 时,有 ,那么 的选择可以由两部分组成:只有一位数的数字和有两位数的数字。对于只有一位数的情况,可以由上述的边界情况计算;对于有两位数的情况,由于第一位数字不能为 ,所以第一位数字有 种选择,第二位数字有 种选择,所以有 种选择,即 种选择。
更一般的情况,含有 位数且各位数字都不同的数字 的个数为 。再加上含有小于 位数且各位数字都不同的数字 的个数,即为答案。
时间复杂度 。
方法二:状态压缩 + 数位 DP
这道题实际上是求在给定区间 中,满足条件的数的个数。条件与数的大小无关,而只与数的组成有关,因此可以使用数位 DP 的思想求解。数位 DP 中,数的大小对复杂度的影响很小。
对于区间 问题,我们一般会将其转化为 然后再减去 的问题,即:
不过对于本题而言,我们只需要求出区间 的值即可。
这里我们用记忆化搜索来实现数位 DP。从起点向下搜索,到最底层得到方案数,一层层向上返回答案并累加,最后从搜索起点得到最终的答案。
我们根据题目信息,设计函数 ,对于本题,我们定义 ,答案为 。
其中:
pos 表示数字的位数,从末位或者第一位开始,一般根据题目的数字构造性质来选择顺序。对于本题,我们选择从高位开始,因此,pos 的初始值为 len;mask 表示当前数字选取了哪些数字(状态压缩);lead 表示当前数字是否含有前导零;关于函数的实现细节,可以参考下面的代码。
时间复杂度 。
相似题目:
class Solution { public int countNumbersWithUniqueDigits(int n) { if (n == 0) { return 1; } if (n == 1) { return 10; } int ans = 10; for (int i = 0, cur = 9; i < n - 1; ++i) { cur *= (9 - i); ans += cur; } return ans; } }
class Solution { private int[][] dp = new int[10][1 << 11]; public int countNumbersWithUniqueDigits(int n) { for (var e : dp) { Arrays.fill(e, -1); } return dfs(n, 0, true); } private int dfs(int pos, int mask, boolean lead) { if (pos <= 0) { return 1; } if (!lead && dp[pos][mask] != -1) { return dp[pos][mask]; } int ans = 0; for (int i = 0; i < 10; ++i) { if (((mask >> i) & 1) == 1) { continue; } if (i == 0 && lead) { ans += dfs(pos - 1, mask, lead); } else { ans += dfs(pos - 1, mask | (1 << i), false); } } if (!lead) { dp[pos][mask] = ans; } return ans; } }
给你一个非空的字符串 s 和一个整数 k ,你要将这个字符串 s 中的字母进行重新排列,使得重排后的字符串中相同字母的位置间隔距离 至少 为 k 。如果无法做到,请返回一个空字符串 ""。
示例 1:
输入: s = "aabbcc", k = 3 输出: "abcabc" 解释: 相同的字母在新的字符串中间隔至少 3 个单位距离。
示例 2:
输入: s = "aaabc", k = 3 输出: "" 解释: 没有办法找到可能的重排结果。
示例 3:
输入: s = "aaadbbcc", k = 2 输出: "abacabcd" 解释: 相同的字母在新的字符串中间隔至少 2 个单位距离。
提示:
1 <= s.length <= 3 * 105s 仅由小写英文字母组成0 <= k <= s.length方法一:贪心 + 哈希表 + 优先队列(大根堆)
先用哈希表 cnt 统计每个字母出现的次数,然后构建一个大根堆 pq,其中每个元素是一个 (v, c) 的元组,其中 c 是字母,v 是字母出现的次数。
重排字符串时,我们每次从堆顶弹出一个元素 (v, c),将 c 添加到结果字符串中,并将 (v-1, c) 放入队列 q 中。当队列 q 的长度达到 及以上时,弹出队首元素,若此时 v 大于 0,则将队首元素放入堆中。循环,直至堆为空。
最后判断结果字符串的长度,若与 s 长度相等,则返回结果字符串,否则返回空串。
时间复杂度 ,其中 是字符串 s 的长度。
相似题目:767. 重构字符串
class Solution { public String rearrangeString(String s, int k) { int n = s.length(); int[] cnt = new int[26]; for (char c : s.toCharArray()) { ++cnt[c - 'a']; } PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[0] - a[0]); for (int i = 0; i < 26; ++i) { if (cnt[i] > 0) { pq.offer(new int[] {cnt[i], i}); } } Deque<int[]> q = new ArrayDeque<>(); StringBuilder ans = new StringBuilder(); while (!pq.isEmpty()) { var p = pq.poll(); int v = p[0], c = p[1]; ans.append((char) ('a' + c)); q.offer(new int[] {v - 1, c}); if (q.size() >= k) { p = q.pollFirst(); if (p[0] > 0) { pq.offer(p); } } } return ans.length() == n ? ans.toString() : ""; } }
请你设计一个日志系统,可以流式接收消息以及它的时间戳。每条 不重复 的消息最多只能每 10 秒打印一次。也就是说,如果在时间戳 t 打印某条消息,那么相同内容的消息直到时间戳变为 t + 10 之前都不会被打印。
所有消息都按时间顺序发送。多条消息可能到达同一时间戳。
实现 Logger 类:
Logger() 初始化 logger 对象bool shouldPrintMessage(int timestamp, string message) 如果这条消息 message 在给定的时间戳 timestamp 应该被打印出来,则返回 true ,否则请返回 false 。示例:
输入: ["Logger", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage"] [[], [1, "foo"], [2, "bar"], [3, "foo"], [8, "bar"], [10, "foo"], [11, "foo"]] 输出: [null, true, true, false, false, false, true] 解释: Logger logger = new Logger(); logger.shouldPrintMessage(1, "foo"); // 返回 true ,下一次 "foo" 可以打印的时间戳是 1 + 10 = 11 logger.shouldPrintMessage(2, "bar"); // 返回 true ,下一次 "bar" 可以打印的时间戳是 2 + 10 = 12 logger.shouldPrintMessage(3, "foo"); // 3 < 11 ,返回 false logger.shouldPrintMessage(8, "bar"); // 8 < 12 ,返回 false logger.shouldPrintMessage(10, "foo"); // 10 < 11 ,返回 false logger.shouldPrintMessage(11, "foo"); // 11 >= 11 ,返回 true ,下一次 "foo" 可以打印的时间戳是 11 + 10 = 21
提示:
0 <= timestamp <= 109timestamp 都将按非递减顺序(时间顺序)传递1 <= message.length <= 30104 次 shouldPrintMessage 方法哈希表实现。
class Logger { private Map<String, Integer> limiter; /** Initialize your data structure here. */ public Logger() { limiter = new HashMap<>(); } /** Returns true if the message should be printed in the given timestamp, otherwise returns false. If this method returns false, the message will not be printed. The timestamp is in seconds granularity. */ public boolean shouldPrintMessage(int timestamp, String message) { int t = limiter.getOrDefault(message, 0); if (t > timestamp) { return false; } limiter.put(message, timestamp + 10); return true; } } /** * Your Logger object will be instantiated and called as such: * Logger obj = new Logger(); * boolean param_1 = obj.shouldPrintMessage(timestamp,message); */
给你一个已经 排好序 的整数数组 nums 和整数 a 、 b 、 c 。对于数组中的每一个元素 nums[i] ,计算函数值 f(x) = ax2 + bx + c ,请 按升序返回数组 。
示例 1:
输入: nums = [-4,-2,2,4], a = 1, b = 3, c = 5 输出: [3,9,15,33]
示例 2:
输入: nums = [-4,-2,2,4], a = -1, b = 3, c = 5 输出: [-23,-5,1,7]
提示:
1 <= nums.length <= 200-100 <= nums[i], a, b, c <= 100nums 按照 升序排列进阶:你可以在时间复杂度为 O(n) 的情况下解决这个问题吗?
双指针。
利用抛物线的性质,i,j 分别指向排序数组首尾,从两端向中间夹逼。
a > 0,抛物线向上,两端具有最大值,比较两端点的较大值,添加到结果数组。a < 0,抛物线向下,两端具有最小值,比较两端点的较小值,添加到结果数组。a == 0,合并到以上的任意一种情况均可。class Solution { public int[] sortTransformedArray(int[] nums, int a, int b, int c) { int n = nums.length; int i = 0, j = n - 1, k = a < 0 ? 0 : n - 1; int[] res = new int[n]; while (i <= j) { int v1 = f(a, b, c, nums[i]), v2 = f(a, b, c, nums[j]); if (a < 0) { if (v1 <= v2) { res[k] = v1; ++i; } else { res[k] = v2; --j; } ++k; } else { if (v1 >= v2) { res[k] = v1; ++i; } else { res[k] = v2; --j; } --k; } } return res; } private int f(int a, int b, int c, int x) { return a * x * x + b * x + c; } }
给你一个大小为 m x n 的矩阵 grid ,其中每个单元格都放置有一个字符:
'W' 表示一堵墙'E' 表示一个敌人'0'(数字 0)表示一个空位返回你使用 一颗炸弹 可以击杀的最大敌人数目。你只能把炸弹放在一个空位里。
由于炸弹的威力不足以穿透墙体,炸弹只能击杀同一行和同一列没被墙体挡住的敌人。
示例 1:
输入:grid = [["0","E","0","0"],["E","0","W","E"],["0","E","0","0"]] 输出:3
示例 2:
输入:grid = [["W","W","W"],["0","0","0"],["E","E","E"]] 输出:1
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 500grid[i][j] 可以是 'W'、'E' 或 '0'class Solution { public int maxKilledEnemies(char[][] grid) { int m = grid.length; int n = grid[0].length; int[][] g = new int[m][n]; for (int i = 0; i < m; ++i) { int t = 0; for (int j = 0; j < n; ++j) { if (grid[i][j] == 'W') { t = 0; } else if (grid[i][j] == 'E') { ++t; } g[i][j] += t; } t = 0; for (int j = n - 1; j >= 0; --j) { if (grid[i][j] == 'W') { t = 0; } else if (grid[i][j] == 'E') { ++t; } g[i][j] += t; } } for (int j = 0; j < n; ++j) { int t = 0; for (int i = 0; i < m; ++i) { if (grid[i][j] == 'W') { t = 0; } else if (grid[i][j] == 'E') { ++t; } g[i][j] += t; } t = 0; for (int i = m - 1; i >= 0; --i) { if (grid[i][j] == 'W') { t = 0; } else if (grid[i][j] == 'E') { ++t; } g[i][j] += t; } } int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '0') { ans = Math.max(ans, g[i][j]); } } } return ans; } }
设计一个敲击计数器,使它可以统计在过去 5 分钟内被敲击次数。(即过去 300 秒)
您的系统应该接受一个时间戳参数 timestamp (单位为 秒 ),并且您可以假定对系统的调用是按时间顺序进行的(即 timestamp 是单调递增的)。几次撞击可能同时发生。
实现 HitCounter 类:
HitCounter() 初始化命中计数器系统。void hit(int timestamp) 记录在 timestamp ( 单位为秒 )发生的一次命中。在同一个 timestamp 中可能会出现几个点击。int getHits(int timestamp) 返回 timestamp 在过去 5 分钟内(即过去 300 秒)的命中次数。示例 1:
输入: ["HitCounter", "hit", "hit", "hit", "getHits", "hit", "getHits", "getHits"] [[], [1], [2], [3], [4], [300], [300], [301]] 输出: [null, null, null, null, 3, null, 4, 3] 解释: HitCounter counter = new HitCounter(); counter.hit(1);// 在时刻 1 敲击一次。 counter.hit(2);// 在时刻 2 敲击一次。 counter.hit(3);// 在时刻 3 敲击一次。 counter.getHits(4);// 在时刻 4 统计过去 5 分钟内的敲击次数, 函数返回 3 。 counter.hit(300);// 在时刻 300 敲击一次。 counter.getHits(300); // 在时刻 300 统计过去 5 分钟内的敲击次数,函数返回 4 。 counter.getHits(301); // 在时刻 301 统计过去 5 分钟内的敲击次数,函数返回 3 。
提示:
1 <= timestamp <= 2 * 109timestamp 是单调递增的)hit and getHits 最多被调用 300 次进阶: 如果每秒的敲击次数是一个很大的数字,你的计数器可以应对吗?
用哈希表作为计数器实现。
class HitCounter { private Map<Integer, Integer> counter; /** Initialize your data structure here. */ public HitCounter() { counter = new HashMap<>(); } /** Record a hit. @param timestamp - The current timestamp (in seconds granularity). */ public void hit(int timestamp) { counter.put(timestamp, counter.getOrDefault(timestamp, 0) + 1); } /** Return the number of hits in the past 5 minutes. @param timestamp - The current timestamp (in seconds granularity). */ public int getHits(int timestamp) { int hits = 0; for (Map.Entry<Integer, Integer> entry : counter.entrySet()) { if (entry.getKey() + 300 > timestamp) { hits += entry.getValue(); } } return hits; } } /** * Your HitCounter object will be instantiated and called as such: * HitCounter obj = new HitCounter(); * obj.hit(timestamp); * int param_2 = obj.getHits(timestamp); */
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。
题目数据保证总会存在一个数值和不超过 k 的矩形区域。
示例 1:
输入:matrix = [[1,0,1],[0,-2,3]], k = 2 输出:2 解释:蓝色边框圈出来的矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
示例 2:
输入:matrix = [[2,2,-1]], k = 3 输出:3
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 100-100 <= matrix[i][j] <= 100-105 <= k <= 105进阶:如果行数远大于列数,该如何设计解决方案?
给你一个整数嵌套列表 nestedList ,每一个元素要么是一个整数,要么是一个列表(这个列表中的每个元素也同样是整数或列表)。
整数的 深度 取决于它位于多少个列表内部。例如,嵌套列表 [1,[2,2],[[3],2],1] 的每个整数的值都等于它的 深度 。令 maxDepth 是任意整数的 最大深度 。
整数的 权重 为 maxDepth - (整数的深度) + 1 。
将 nestedList 列表中每个整数先乘权重再求和,返回该加权和。
示例 1:
输入:nestedList = [[1,1],2,[1,1]] 输出:8 解释:4 个 1 在深度为 1 的位置, 一个 2 在深度为 2 的位置。 1*1 + 1*1 + 2*2 + 1*1 + 1*1 = 8
示例 2:
输入:nestedList = [1,[4,[6]]] 输出:17 解释:一个 1 在深度为 3 的位置, 一个 4 在深度为 2 的位置,一个 6 在深度为 1 的位置。 1*3 + 4*2 + 6*1 = 17
提示:
1 <= nestedList.length <= 50[-100, 100]50先求序列的最大深度 depth,然后利用 DFS 累加求和。
/** * // This is the interface that allows for creating nested lists. * // You should not implement it, or speculate about its implementation * public interface NestedInteger { * // Constructor initializes an empty nested list. * public NestedInteger(); * * // Constructor initializes a single integer. * public NestedInteger(int value); * * // @return true if this NestedInteger holds a single integer, rather than a nested list. * public boolean isInteger(); * * // @return the single integer that this NestedInteger holds, if it holds a single integer * // Return null if this NestedInteger holds a nested list * public Integer getInteger(); * * // Set this NestedInteger to hold a single integer. * public void setInteger(int value); * * // Set this NestedInteger to hold a nested list and adds a nested integer to it. * public void add(NestedInteger ni); * * // @return the nested list that this NestedInteger holds, if it holds a nested list * // Return empty list if this NestedInteger holds a single integer * public List<NestedInteger> getList(); * } */ class Solution { public int depthSumInverse(List<NestedInteger> nestedList) { int depth = maxDepth(nestedList); return dfs(nestedList, depth); } private int maxDepth(List<NestedInteger> nestedList) { int depth = 1; for (NestedInteger item : nestedList) { if (item.isInteger()) { continue; } depth = Math.max(depth, 1 + maxDepth(item.getList())); } return depth; } private int dfs(List<NestedInteger> nestedList, int depth) { int depthSum = 0; for (NestedInteger item : nestedList) { if (item.isInteger()) { depthSum += item.getInteger() * depth; } else { depthSum += dfs(item.getList(), depth - 1); } } return depthSum; } }
有两个水壶,容量分别为 jug1Capacity 和 jug2Capacity 升。水的供应是无限的。确定是否有可能使用这两个壶准确得到 targetCapacity 升。
如果可以得到 targetCapacity 升水,最后请用以上水壶中的一或两个来盛放取得的 targetCapacity 升水。
你可以:
示例 1:
输入: jug1Capacity = 3, jug2Capacity = 5, targetCapacity = 4 输出: true 解释:来自著名的 "Die Hard"
示例 2:
输入: jug1Capacity = 2, jug2Capacity = 6, targetCapacity = 5 输出: false
示例 3:
输入: jug1Capacity = 1, jug2Capacity = 2, targetCapacity = 3 输出: true
提示:
1 <= jug1Capacity, jug2Capacity, targetCapacity <= 106可以认为,每次操作只会让壶里的水带来 x 或者 y 的变化量。因此只要满足 ax + by = z 即可。
根据裴蜀定理,ax + by = z 有解,当且仅当 z 是 x,y 的最大公约数的倍数。所以我们只要找到 x,y 的最大公约数,然后判断 z 是否是这个最大公约数的倍数即可求得答案。
class Solution { public boolean canMeasureWater(int jug1Capacity, int jug2Capacity, int targetCapacity) { Deque<int[]> stk = new ArrayDeque<>(); stk.add(new int[]{0, 0}); Set<Long> seen = new HashSet<>(); while (!stk.isEmpty()) { if (seen.contains(hash(stk.peek()))) { stk.pop(); continue; } int[] cur = stk.pop(); seen.add(hash(cur)); int cur1 = cur[0], cur2 = cur[1]; if (cur1 == targetCapacity || cur2 == targetCapacity || cur1 + cur2 == targetCapacity) { return true; } stk.offer(new int[]{jug1Capacity, cur2}); stk.offer(new int[]{0, cur2}); stk.offer(new int[]{cur1, jug1Capacity}); stk.offer(new int[]{cur2, 0}); if (cur1 + cur2 > jug1Capacity) { stk.offer(new int[]{jug1Capacity, cur2 - jug1Capacity + cur1}); } else { stk.offer(new int[]{cur1 + cur2, 0}); } if (cur1 + cur2 > jug2Capacity) { stk.offer(new int[]{cur1 - jug2Capacity + cur2, jug2Capacity}); } else { stk.offer(new int[]{0, cur1 + cur2}); } } return false; } public long hash(int[] nums) { return nums[0] * 10000006L + nums[1]; } }
class Solution { public boolean canMeasureWater(int jug1Capacity, int jug2Capacity, int targetCapacity) { if (jug1Capacity + jug2Capacity < targetCapacity) { return false; } if (jug1Capacity == 0 || jug2Capacity == 0) { return targetCapacity == 0 || jug1Capacity + jug2Capacity == targetCapacity; } return targetCapacity % gcd(jug1Capacity, jug2Capacity) == 0; } private int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } }
给你一棵二叉树,请按以下要求的顺序收集它的全部节点:
示例:
输入: [1,2,3,4,5]
1
/ \
2 3
/ \
4 5
输出: [[4,5,3],[2],[1]]
解释:
1. 删除叶子节点 [4,5,3] ,得到如下树结构:
1
/
2
2. 现在删去叶子节点 [2] ,得到如下树结构:
1
3. 现在删去叶子节点 [1] ,得到空树:
[]
添加前置节点 prev,初始时 prev.left = root。
class Solution { public List<List<Integer>> findLeaves(TreeNode root) { List<List<Integer>> res = new ArrayList<>(); TreeNode prev = new TreeNode(0, root, null); while (prev.left != null) { List<Integer> t = new ArrayList<>(); dfs(prev.left, prev, t); res.add(t); } return res; } private void dfs(TreeNode root, TreeNode prev, List<Integer> t) { if (root == null) { return; } if (root.left == null && root.right == null) { t.add(root.val); if (prev.left == root) { prev.left = null; } else { prev.right = null; } } dfs(root.left, root, t); dfs(root.right, root, t); } }
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。
示例 1:
输入:num = 16 输出:true 解释:返回 true ,因为 4 * 4 = 16 且 4 是一个整数。
示例 2:
输入:num = 14 输出:false 解释:返回 false ,因为 3.742 * 3.742 = 14 但 3.742 不是一个整数。
提示:
1 <= num <= 231 - 1方法一:二分查找
不断循环二分枚举数字,判断该数的平方与 num 的大小关系,进而缩短空间,继续循环直至 不成立。循环结束判断 与 num 是否相等。
时间复杂度:。
方法二:转换为数学问题
由于 n² = 1 + 3 + 5 + ... + (2n-1),对数字 num 不断减去 (i = 1, 3, 5, ...) 直至 num 不大于 0,如果最终 num 等于 0,说明是一个有效的完全平方数。
时间复杂度:。
class Solution { public boolean isPerfectSquare(int num) { long left = 1, right = num; while (left < right) { long mid = (left + right) >>> 1; if (mid * mid >= num) { right = mid; } else { left = mid + 1; } } return left * left == num; } }
class Solution { public boolean isPerfectSquare(int num) { for (int i = 1; num > 0; i += 2) { num -= i; } return num == 0; } }
给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:
answer[i] % answer[j] == 0 ,或answer[j] % answer[i] == 0如果存在多个有效解子集,返回其中任何一个均可。
示例 1:
输入:nums = [1,2,3] 输出:[1,2] 解释:[1,3] 也会被视为正确答案。
示例 2:
输入:nums = [1,2,4,8] 输出:[1,2,4,8]
提示:
1 <= nums.length <= 10001 <= nums[i] <= 2 * 109nums 中的所有整数 互不相同方法一:排序 + 动态规划
我们先对数组进行排序,这样可以保证对于任意的 ,如果 可以整除 ,那么 一定在 的左边。
接下来,我们定义 表示以 为最大元素的最大整除子集的大小,初始时 。
对于每一个 ,我们从左往右枚举 ,如果 可以被 整除,那么 可以从 转移而来,我们更新 。过程中,我们记录 的最大值的下标 以及对应的子集大小 。
最后,我们从 开始倒序遍历,如果 可以被 整除,且 ,那么 就是一个整除子集的元素,我们将 加入答案,并将 减 ,同时更新 。继续倒序遍历,直到 。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
class Solution { public List<Integer> largestDivisibleSubset(int[] nums) { Arrays.sort(nums); int n = nums.length; int[] f = new int[n]; Arrays.fill(f, 1); int k = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { if (nums[i] % nums[j] == 0) { f[i] = Math.max(f[i], f[j] + 1); } } if (f[k] < f[i]) { k = i; } } int m = f[k]; List<Integer> ans = new ArrayList<>(); for (int i = k; m > 0; --i) { if (nums[k] % nums[i] == 0 && f[i] == m) { ans.add(nums[i]); k = i; --m; } } return ans; } }
给定一个用链表表示的非负整数, 然后将这个整数 再加上 1 。
这些数字的存储是这样的:最高位有效的数字位于链表的首位 head 。
示例 1:
输入: head = [1,2,3] 输出: [1,2,4]
示例 2:
输入: head = [0] 输出: [1]
提示:
[1, 100] 的范围内。0 <= Node.val <= 9方法一:链表遍历
我们先设置一个虚拟头节点 dummy,初始值为 ,指向链表头节点 head。
然后从链表头节点开始遍历,找出链表最后一个值不等于 的节点 target,将 target 的值加 。接着将 target 之后的所有节点值置为 。
需要注意的是,如果链表中所有节点值都为 ,那么遍历结束后,target 会指向空节点,这时我们需要将 dummy 的值加 ,然后返回 dummy,否则返回 dummy 的下一个节点。
时间复杂度 ,空间复杂度 。其中 为链表的长度。
class Solution { public ListNode plusOne(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode target = dummy; while (head != null) { if (head.val != 9) { target = head; } head = head.next; } ++target.val; target = target.next; while (target != null) { target.val = 0; target = target.next; } return dummy.val == 1 ? dummy : dummy.next; } }
假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k 个更新的操作。
其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex ... endIndex](包括 startIndex 和 endIndex)增加 inc。
请你返回 k 次操作后的数组。
示例:
输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3]
解释:
初始状态: [0,0,0,0,0] 进行了操作 [1,3,2] 后的状态: [0,2,2,2,0] 进行了操作 [2,4,3] 后的状态: [0,2,5,5,3] 进行了操作 [0,2,-2] 后的状态: [-2,0,3,5,3]
方法一:差分数组
差分数组模板题。
我们定义 为差分数组。给区间 中的每一个数加上 ,那么有 ,并且 。最后我们对差分数组求前缀和,即可得到原数组。
时间复杂度 ,空间复杂度 。其中 为数组长度。
方法二:树状数组 + 差分思想
时间复杂度 。
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 位置的数加上一个值 ;query(x):查询序列 区间的区间和,即位置 的前缀和。这两个操作的时间复杂度均为 。
差分数组:
树状数组:
差分数组:
class Solution { public int[] getModifiedArray(int length, int[][] updates) { int[] d = new int[length]; for (var e : updates) { int l = e[0], r = e[1], c = e[2]; d[l] += c; if (r + 1 < length) { d[r + 1] -= c; } } for (int i = 1; i < length; ++i) { d[i] += d[i - 1]; } return d; } }
树状数组:
class Solution { public int[] getModifiedArray(int length, int[][] updates) { BinaryIndexedTree tree = new BinaryIndexedTree(length); for (int[] e : updates) { int start = e[0], end = e[1], inc = e[2]; tree.update(start + 1, inc); tree.update(end + 2, -inc); } int[] ans = new int[length]; for (int i = 0; i < length; ++i) { ans[i] = tree.query(i + 1); } return ans; } } class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } }
差分数组:
树状数组:
差分数组:
树状数组:
给你两个整数 a 和 b ,不使用 运算符 + 和 - ,计算并返回两整数之和。
示例 1:
输入:a = 1, b = 2 输出:3
示例 2:
输入:a = 2, b = 3 输出:5
提示:
-1000 <= a, b <= 1000方法一:位运算
两数字的二进制形式 a,b ,求和 s = a + b ,a(i)、b(i) 分别表示 a、b 的第 i 个二进制位。一共有 4 种情况:
| a(i) | b(i) | 不进位的和 | 进位 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
观察可以发现,“不进位的和”与“异或运算”有相同规律,而进位则与“与”运算规律相同,并且需要左移一位。
^ 异或运算,得到不进位的和;& 与运算,然后左移一位,得到进位;时间复杂度 。
由于 python int 是无限长整型,左移不会自动溢出,因此需要特殊处理。
class Solution { public int getSum(int a, int b) { return b == 0 ? a : getSum(a ^ b, (a & b) << 1); } }
你的任务是计算 ab 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。
示例 1:
输入:a = 2, b = [3] 输出:8
示例 2:
输入:a = 2, b = [1,0] 输出:1024
示例 3:
输入:a = 1, b = [4,3,3,8,5,2] 输出:1
示例 4:
输入:a = 2147483647, b = [2,0,0] 输出:1198
提示:
1 <= a <= 231 - 11 <= b.length <= 20000 <= b[i] <= 9b 不含前导 0乘方快速幂。
class Solution { private static final int MOD = 1337; public int superPow(int a, int[] b) { int ans = 1; for (int i = b.length - 1; i >= 0; --i) { ans = (int) ((long) ans * quickPowAndMod(a, b[i]) % MOD); a = quickPowAndMod(a, 10); } return ans; } private int quickPowAndMod(int a, int b) { int ans = 1; while (b > 0) { if ((b & 1) == 1) { ans = (ans * (a % MOD)) % MOD; } b >>= 1; a = (a % MOD) * (a % MOD) % MOD; } return ans; } }
给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk) 。
示例 1:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
示例 2:
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2 输出: [1,1],[1,1] 解释: 返回序列中的前 2 对数: [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
示例 3:
输入: nums1 = [1,2], nums2 = [3], k = 3 输出: [1,3],[2,3] 解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]
提示:
1 <= nums1.length, nums2.length <= 105-109 <= nums1[i], nums2[i] <= 109nums1 和 nums2 均为升序排列1 <= k <= 1000
class Solution { public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) { PriorityQueue<int[]> q = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); for (int i = 0; i < Math.min(nums1.length, k); ++i) { q.offer(new int[] {nums1[i] + nums2[0], i, 0}); } List<List<Integer>> ans = new ArrayList<>(); while (!q.isEmpty() && k > 0) { int[] e = q.poll(); ans.add(Arrays.asList(nums1[e[1]], nums2[e[2]])); --k; if (e[2] + 1 < nums2.length) { q.offer(new int[] {nums1[e[1]] + nums2[e[2] + 1], e[1], e[2] + 1}); } } return ans; } }
猜数字游戏的规则如下:
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
pick < numpick > numpick == num返回我选出的数字。
示例 1:
输入:n = 10, pick = 6 输出:6
示例 2:
输入:n = 1, pick = 1 输出:1
示例 3:
输入:n = 2, pick = 1 输出:1
示例 4:
输入:n = 2, pick = 2 输出:2
提示:
1 <= n <= 231 - 11 <= pick <= n方法一:二分查找
我们在区间 进行二分查找,找到第一个满足 guess(x) <= 0 的数,即为答案。
时间复杂度 。其中 为题目给定的上限。
/** * Forward declaration of guess API. * @param num your guess * @return -1 if num is lower than the guess number * 1 if num is higher than the guess number * otherwise return 0 * int guess(int num); */ public class Solution extends GuessGame { public int guessNumber(int n) { int left = 1, right = n; while (left < right) { int mid = (left + right) >>> 1; if (guess(mid) <= 0) { right = mid; } else { left = mid + 1; } } return left; } }
我们正在玩一个猜数游戏,游戏规则如下:
1 到 n 之间选择一个数字。x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
输入:n = 10 输出:16 解释:制胜策略如下: - 数字范围是 [1,10] 。你先猜测数字为 7 。 - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。 - 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。 - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。 - 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。 - 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。 - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。 - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。 - 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。 - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。 - 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。 - 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。 - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。 - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。 - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。 在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
示例 2:
输入:n = 1 输出:0 解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
示例 3:
输入:n = 2 输出:1 解释:有两个可能的数字 1 和 2 。 - 你可以先猜 1 。 - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。 - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。 最糟糕的情况下,你需要支付 $1 。
提示:
1 <= n <= 200区间 DP。
dp[i][j] 表示数字区间 [i, j] 确保赢得游戏的最少现金。[i, j] 中以数字 k 作为选择的数字。那么 dp[i][j] = min(dp[i][j], max(dp[i][k - 1], dp[k + 1][j]) + k), k ∈ [i, j]。以区间长度 l 从小到大开始处理每个状态值。
class Solution { public int getMoneyAmount(int n) { int[][] dp = new int[n + 10][n + 10]; for (int l = 2; l <= n; ++l) { for (int i = 1; i + l - 1 <= n; ++i) { int j = i + l - 1; dp[i][j] = Integer.MAX_VALUE; for (int k = i; k <= j; ++k) { int t = Math.max(dp[i][k - 1], dp[k + 1][j]) + k; dp[i][j] = Math.min(dp[i][j], t); } } } return dp[1][n]; } }
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5] 输出:6 解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8] 输出:7 解释:这个序列包含几个长度为 7 摆动序列。 其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9] 输出:2
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000进阶:你能否用 O(n) 时间复杂度完成此题?
动态规划。
设 up 表示以前 i 个元素中的某一个元素结尾的最长上升摆动序列的长度,down 表示以前 i 个元素中的某一个元素结尾的最长下降摆动序列的长度。初始 up = 1, down = 1。
从数组下标 1 开始遍历:
nums[i] > nums[i - 1],则需要更新最长上升摆动序列的长度:up = max(up, down + 1)nums[i] < nums[i - 1],则需要更新最长下降摆动序列的长度:down = max(down, up + 1)最后返回 max(up, down) 即可。
class Solution { public int wiggleMaxLength(int[] nums) { int up = 1, down = 1; for (int i = 1; i < nums.length; ++i) { if (nums[i] > nums[i - 1]) { up = Math.max(up, down + 1); } else if (nums[i] < nums[i - 1]) { down = Math.max(down, up + 1); } } return Math.max(up, down); } }
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3 输出:0
提示:
1 <= nums.length <= 2001 <= nums[i] <= 1000nums 中的所有元素 互不相同1 <= target <= 1000进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
方法一:动态规划
我们定义 表示总和为 的元素组合的个数,初始时 ,其余 。最终答案即为 。
对于 ,我们可以枚举数组中的每个元素 ,如果 ,则 。
最后返回 即可。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { public int combinationSum4(int[] nums, int target) { int[] f = new int[target + 1]; f[0] = 1; for (int i = 1; i <= target; ++i) { for (int x : nums) { if (i >= x) { f[i] += f[i - x]; } } } return f[target]; } }
给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于 O(n2) 的解决方案。
示例 1:
输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 输出:13 解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13
示例 2:
输入:matrix = [[-5]], k = 1 输出:-5
提示:
n == matrix.lengthn == matrix[i].length1 <= n <= 300-109 <= matrix[i][j] <= 109matrix 中的所有行和列都按 非递减顺序 排列1 <= k <= n2进阶:
O(1) 内存复杂度)来解决这个问题?O(n) 的时间复杂度下解决这个问题吗?这个方法对于面试来说可能太超前了,但是你会发现阅读这篇文章( this paper )很有趣。二分法。
class Solution { public int kthSmallest(int[][] matrix, int k) { int n = matrix.length; int left = matrix[0][0], right = matrix[n - 1][n - 1]; while (left < right) { int mid = (left + right) >>> 1; if (check(matrix, mid, k, n)) { right = mid; } else { left = mid + 1; } } return left; } private boolean check(int[][] matrix, int mid, int k, int n) { int count = 0; int i = n - 1, j = 0; while (i >= 0 && j < n) { if (matrix[i][j] <= mid) { count += (i + 1); ++j; } else { --i; } } return count >= k; } }
设计一个电话目录管理系统,让它支持以下功能:
get: 分配给用户一个未被使用的电话号码,获取失败请返回 -1check: 检查指定的电话号码是否被使用release: 释放掉一个电话号码,使其能够重新被分配示例:
// 初始化电话目录,包括 3 个电话号码:0,1 和 2。 PhoneDirectory directory = new PhoneDirectory(3); // 可以返回任意未分配的号码,这里我们假设它返回 0。 directory.get(); // 假设,函数返回 1。 directory.get(); // 号码 2 未分配,所以返回为 true。 directory.check(2); // 返回 2,分配后,只剩一个号码未被分配。 directory.get(); // 此时,号码 2 已经被分配,所以返回 false。 directory.check(2); // 释放号码 2,将该号码变回未分配状态。 directory.release(2); // 号码 2 现在是未分配状态,所以返回 true。 directory.check(2);
提示:
1 <= maxNumbers <= 10^40 <= number < maxNumbers[0 - 20000] 之内class PhoneDirectory { private boolean[] provided; /** Initialize your data structure here @param maxNumbers - The maximum numbers that can be stored in the phone directory. */ public PhoneDirectory(int maxNumbers) { provided = new boolean[maxNumbers]; } /** Provide a number which is not assigned to anyone. @return - Return an available number. Return -1 if none is available. */ public int get() { for (int i = 0; i < provided.length; ++i) { if (!provided[i]) { provided[i] = true; return i; } } return -1; } /** Check if a number is available or not. */ public boolean check(int number) { return !provided[number]; } /** Recycle or release a number. */ public void release(int number) { provided[number] = false; } } /** * Your PhoneDirectory object will be instantiated and called as such: * PhoneDirectory obj = new PhoneDirectory(maxNumbers); * int param_1 = obj.get(); * boolean param_2 = obj.check(number); * obj.release(number); */
实现RandomizedSet 类:
RandomizedSet() 初始化 RandomizedSet 对象bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。
示例:
输入 ["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] [[], [1], [2], [2], [], [1], [2], []] 输出 [null, true, false, true, 2, true, false, 2] 解释 RandomizedSet randomizedSet = new RandomizedSet(); randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。 randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。 randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。 randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。 randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。 randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。 randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
提示:
-231 <= val <= 231 - 1insert、remove 和 getRandom 函数 2 * 105 次getRandom 方法时,数据结构中 至少存在一个 元素。方法一:哈希表 + 动态列表
我们定义一个动态列表 ,用于存储集合中的元素,定义一个哈希表 ,用于存储每个元素在 中的下标。
插入元素时,如果元素已经存在于哈希表 中,直接返回 false;否则,我们将元素插入到动态列表 的末尾,同时将元素和其在 中的下标插入到哈希表 中,最后返回 true。
删除元素时,如果元素不存在于哈希表 中,直接返回 false;否则,我们从哈希表中获取元素在列表 中的下标 ,然后将列表 的最后一个元素 与 交换,然后将哈希表中 的下标更新为 ,最后将 的最后一个元素删除,同时将元素从哈希表中删除,最后返回 true。
获取随机元素时,我们从动态列表 中随机选择一个元素返回即可。
时间复杂度 ,空间复杂度 。其中 为集合中元素的个数。
class RandomizedSet { private Map<Integer, Integer> d = new HashMap<>(); private List<Integer> q = new ArrayList<>(); private Random rnd = new Random(); public RandomizedSet() { } public boolean insert(int val) { if (d.containsKey(val)) { return false; } d.put(val, q.size()); q.add(val); return true; } public boolean remove(int val) { if (!d.containsKey(val)) { return false; } int i = d.get(val); d.put(q.get(q.size() - 1), i); q.set(i, q.get(q.size() - 1)); q.remove(q.size() - 1); d.remove(val); return true; } public int getRandom() { return q.get(rnd.nextInt(q.size())); } } /** * Your RandomizedSet object will be instantiated and called as such: * RandomizedSet obj = new RandomizedSet(); * boolean param_1 = obj.insert(val); * boolean param_2 = obj.remove(val); * int param_3 = obj.getRandom(); */
RandomizedCollection 是一种包含数字集合(可能是重复的)的数据结构。它应该支持插入和删除特定元素,以及删除随机元素。
实现 RandomizedCollection 类:
RandomizedCollection()初始化空的 RandomizedCollection 对象。bool insert(int val) 将一个 val 项插入到集合中,即使该项已经存在。如果该项不存在,则返回 true ,否则返回 false 。bool remove(int val) 如果存在,从集合中移除一个 val 项。如果该项存在,则返回 true ,否则返回 false 。注意,如果 val 在集合中出现多次,我们只删除其中一个。int getRandom() 从当前的多个元素集合中返回一个随机元素。每个元素被返回的概率与集合中包含的相同值的数量 线性相关 。您必须实现类的函数,使每个函数的 平均 时间复杂度为 O(1) 。
注意:生成测试用例时,只有在 RandomizedCollection 中 至少有一项 时,才会调用 getRandom 。
示例 1:
输入
["RandomizedCollection", "insert", "insert", "insert", "getRandom", "remove", "getRandom"]
[[], [1], [1], [2], [], [1], []]
输出
[null, true, false, true, 2, true, 1]
解释
RandomizedCollection collection = new RandomizedCollection();// 初始化一个空的集合。
collection.insert(1); // 返回 true,因为集合不包含 1。
// 将 1 插入到集合中。
collection.insert(1); // 返回 false,因为集合包含 1。
// 将另一个 1 插入到集合中。集合现在包含 [1,1]。
collection.insert(2); // 返回 true,因为集合不包含 2。
// 将 2 插入到集合中。集合现在包含 [1,1,2]。
collection.getRandom(); // getRandom 应当:
// 有 2/3 的概率返回 1,
// 1/3 的概率返回 2。
collection.remove(1); // 返回 true,因为集合包含 1。
// 从集合中移除 1。集合现在包含 [1,2]。
collection.getRandom(); // getRandom 应该返回 1 或 2,两者的可能性相同。
提示:
-231 <= val <= 231 - 1insert, remove 和 getRandom 最多 总共 被调用 2 * 105 次getRandom 时,数据结构中 至少有一个 元素“哈希表 + 动态列表”实现。
哈希表存放每个元素的值和对应的下标集合,而动态列表在每个下标位置存放每个元素。由动态列表实现元素的随机返回。
注意,在 remove() 实现上,将列表的最后一个元素设置到待删元素的位置上,然后删除最后一个元素,这样在删除元素的时候,不需要挪动一大批元素,从而实现 O(1) 时间内操作。
class RandomizedCollection { private Map<Integer, Set<Integer>> m; private List<Integer> l; private Random rnd; /** Initialize your data structure here. */ public RandomizedCollection() { m = new HashMap<>(); l = new ArrayList<>(); rnd = new Random(); } /** * Inserts a value to the collection. Returns true if the collection did not already contain * the specified element. */ public boolean insert(int val) { m.computeIfAbsent(val, k -> new HashSet<>()).add(l.size()); l.add(val); return m.get(val).size() == 1; } /** * Removes a value from the collection. Returns true if the collection contained the specified * element. */ public boolean remove(int val) { if (!m.containsKey(val)) { return false; } Set<Integer> idxSet = m.get(val); int idx = idxSet.iterator().next(); int lastIdx = l.size() - 1; l.set(idx, l.get(lastIdx)); idxSet.remove(idx); Set<Integer> lastIdxSet = m.get(l.get(lastIdx)); lastIdxSet.remove(lastIdx); if (idx < lastIdx) { lastIdxSet.add(idx); } if (idxSet.isEmpty()) { m.remove(val); } l.remove(lastIdx); return true; } /** Get a random element from the collection. */ public int getRandom() { int size = l.size(); return size == 0 ? -1 : l.get(rnd.nextInt(size)); } } /** * Your RandomizedCollection object will be instantiated and called as such: * RandomizedCollection obj = new RandomizedCollection(); * boolean param_1 = obj.insert(val); * boolean param_2 = obj.remove(val); * int param_3 = obj.getRandom(); */
给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样 。
实现 Solution 类:
Solution(ListNode head) 使用整数数组初始化对象。int getRandom() 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。示例:
输入 ["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"] [[[1, 2, 3]], [], [], [], [], []] 输出 [null, 1, 3, 2, 2, 3]解释
Solution solution = new Solution([1, 2, 3]);
solution.getRandom(); // 返回 1
solution.getRandom(); // 返回 3
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 3
// getRandom() 方法应随机返回 1、2、3 中的一个,每个元素被返回的概率相等。提示:
[1, 104] 内-104 <= Node.val <= 104getRandom 方法 104 次进阶:
蓄水池抽样问题。即从一个包含 n 个对象的列表 S 中随机选取 k 个对象,n 为一个非常大或者不知道的值。通常情况下,n 是一个非常大的值,大到无法一次性把所有列表 S 中的对象都放到内存中。我们这个问题是蓄水池抽样问题的一个特例,即 k=1。
解法:我们总是选择第一个对象,以 1/2 的概率选择第二个,以 1/3 的概率选择第三个,以此类推,以 1/m 的概率选择第 m 个对象。当该过程结束时,每一个对象具有相同的选中概率,即 1/n。
证明:第 m 个对象最终被选中的概率 P = 选择 m 的概率 × 其后面所有对象不被选择的概率,即:
思路同:398. 随机数索引
class Solution { private ListNode head; private Random random = new Random(); public Solution(ListNode head) { this.head = head; } public int getRandom() { int ans = 0, n = 0; for (ListNode node = head; node != null; node = node.next) { ++n; int x = 1 + random.nextInt(n); if (n == x) { ans = node.val; } } return ans; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(head); * int param_1 = obj.getRandom(); */
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b" 输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab" 输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab" 输出:true
提示:
1 <= ransomNote.length, magazine.length <= 105ransomNote 和 magazine 由小写英文字母组成方法一:哈希表或数组
我们可以用一个哈希表或长度为 的数组 记录字符串 magazine 中所有字符出现的次数。然后遍历字符串 ransomNote,对于其中的每个字符 ,我们将其从 的次数减 ,如果减 之后的次数小于 ,说明 在 magazine 中出现的次数不够,因此无法构成 ransomNote,返回 即可。
否则,遍历结束后,说明 ransomNote 中的每个字符都可以在 magazine 中找到对应的字符,因此返回 。
时间复杂度 ,空间复杂度 。其中 和 分别为字符串 ransomNote 和 magazine 的长度;而 为字符集的大小,本题中 。
class Solution { public boolean canConstruct(String ransomNote, String magazine) { int[] cnt = new int[26]; for (int i = 0; i < magazine.length(); ++i) { ++cnt[magazine.charAt(i) - 'a']; } for (int i = 0; i < ransomNote.length(); ++i) { if (--cnt[ransomNote.charAt(i) - 'a'] < 0) { return false; } } return true; } }
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums) 使用整数数组 nums 初始化对象int[] reset() 重设数组到它的初始状态并返回int[] shuffle() 返回数组随机打乱后的结果示例 1:
输入 ["Solution", "shuffle", "reset", "shuffle"] [[[1, 2, 3]], [], [], []] 输出 [null, [3, 1, 2], [1, 2, 3], [1, 3, 2]] 解释 Solution solution = new Solution([1, 2, 3]); solution.shuffle(); // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2] solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3] solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]
提示:
1 <= nums.length <= 50-106 <= nums[i] <= 106nums 中的所有元素都是 唯一的104 次 reset 和 shuffleclass Solution { private int[] nums; private int[] original; private Random rand; public Solution(int[] nums) { this.nums = nums; this.original = Arrays.copyOf(nums, nums.length); this.rand = new Random(); } public int[] reset() { nums = Arrays.copyOf(original, original.length); return nums; } public int[] shuffle() { for (int i = 0; i < nums.length; ++i) { swap(i, i + rand.nextInt(nums.length - i)); } return nums; } private void swap(int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(nums); * int[] param_1 = obj.reset(); * int[] param_2 = obj.shuffle(); */
给定一个字符串 s 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 NestedInteger 。
列表中的每个元素只可能是整数或整数嵌套列表
示例 1:
输入:s = "324", 输出:324 解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。
示例 2:
输入:s = "[123,[456,[789]]]",
输出:[123,[456,[789]]]
解释:返回一个 NestedInteger 对象包含一个有两个元素的嵌套列表:
1. 一个 integer 包含值 123
2. 一个包含两个元素的嵌套列表:
i. 一个 integer 包含值 456
ii. 一个包含一个元素的嵌套列表
a. 一个 integer 包含值 789
提示:
1 <= s.length <= 5 * 104s 由数字、方括号 "[]"、负号 '-' 、逗号 ','组成s 是可解析的 NestedInteger[-106, 106]方法一:递归
我们首先判断字符串 是否为空或是一个空列表,如果是的话,直接返回一个空的 NestedInteger 即可。如果 是一个整数,我们直接返回一个包含这个整数的 NestedInteger。否则,我们从左到右遍历字符串 ,如果当前深度为 ,并且遇到了逗号或者字符串 的末尾,则我们截取出一个子串并递归调用函数解析该子串,将返回值加入到列表中。否则,如果当前遇到了左括号,我们将深度加 ,并继续遍历。如果遇到了右括号,我们将深度减 ,继续遍历。
遍历结束后,返回答案。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
方法二:栈
我们可以使用栈来模拟递归的过程。
我们首先判断字符串 是否是一个整数,如果是,直接返回一个包含这个整数的 NestedInteger。否则,我们从左到右遍历字符串 ,对于当前遍历到的字符 :
true;NestedInteger 压入栈中;NestedInteger 中,然后将负号标识置为 false,当前数字 重置为 。如果 是右括号,并且当前栈的大小大于 ,我们将栈顶的 NestedInteger 出栈,将其加入到栈顶的 NestedInteger 中。遍历结束后,返回栈顶的 NestedInteger 即可。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
/** * // This is the interface that allows for creating nested lists. * // You should not implement it, or speculate about its implementation * public interface NestedInteger { * // Constructor initializes an empty nested list. * public NestedInteger(); * * // Constructor initializes a single integer. * public NestedInteger(int value); * * // @return true if this NestedInteger holds a single integer, rather than a nested list. * public boolean isInteger(); * * // @return the single integer that this NestedInteger holds, if it holds a single integer * // Return null if this NestedInteger holds a nested list * public Integer getInteger(); * * // Set this NestedInteger to hold a single integer. * public void setInteger(int value); * * // Set this NestedInteger to hold a nested list and adds a nested integer to it. * public void add(NestedInteger ni); * * // @return the nested list that this NestedInteger holds, if it holds a nested list * // Return empty list if this NestedInteger holds a single integer * public List<NestedInteger> getList(); * } */ class Solution { public NestedInteger deserialize(String s) { if ("".equals(s) || "[]".equals(s)) { return new NestedInteger(); } if (s.charAt(0) != '[') { return new NestedInteger(Integer.parseInt(s)); } NestedInteger ans = new NestedInteger(); int depth = 0; for (int i = 1, j = 1; i < s.length(); ++i) { if (depth == 0 && (s.charAt(i) == ',' || i == s.length() - 1)) { ans.add(deserialize(s.substring(j, i))); j = i + 1; } else if (s.charAt(i) == '[') { ++depth; } else if (s.charAt(i) == ']') { --depth; } } return ans; } }
/** * // This is the interface that allows for creating nested lists. * // You should not implement it, or speculate about its implementation * public interface NestedInteger { * // Constructor initializes an empty nested list. * public NestedInteger(); * * // Constructor initializes a single integer. * public NestedInteger(int value); * * // @return true if this NestedInteger holds a single integer, rather than a nested list. * public boolean isInteger(); * * // @return the single integer that this NestedInteger holds, if it holds a single integer * // Return null if this NestedInteger holds a nested list * public Integer getInteger(); * * // Set this NestedInteger to hold a single integer. * public void setInteger(int value); * * // Set this NestedInteger to hold a nested list and adds a nested integer to it. * public void add(NestedInteger ni); * * // @return the nested list that this NestedInteger holds, if it holds a nested list * // Return empty list if this NestedInteger holds a single integer * public List<NestedInteger> getList(); * } */ class Solution { public NestedInteger deserialize(String s) { if (s.charAt(0) != '[') { return new NestedInteger(Integer.parseInt(s)); } Deque<NestedInteger> stk = new ArrayDeque<>(); int x = 0; boolean neg = false; for (int i = 0; i < s.length(); ++i) { char c = s.charAt(i); if (c == '-') { neg = true; } else if (Character.isDigit(c)) { x = x * 10 + c - '0'; } else if (c == '[') { stk.push(new NestedInteger()); } else if (c == ',' || c == ']') { if (Character.isDigit(s.charAt(i - 1))) { if (neg) { x = -x; } stk.peek().add(new NestedInteger(x)); } x = 0; neg = false; if (c == ']' && stk.size() > 1) { NestedInteger t = stk.pop(); stk.peek().add(t); } } } return stk.peek(); } }
给你一个整数 n ,按字典序返回范围 [1, n] 内所有整数。
你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。
示例 1:
输入:n = 13 输出:[1,10,11,12,13,2,3,4,5,6,7,8,9]
示例 2:
输入:n = 2 输出:[1,2]
提示:
1 <= n <= 5 * 104方法一:DFS
class Solution { public List<Integer> lexicalOrder(int n) { List<Integer> ans = new ArrayList<>(); for (int i = 1; i < 10; ++i) { dfs(i, n, ans); } return ans; } private void dfs(int u, int n, List<Integer> ans) { if (u > n) { return; } ans.add(u); for (int i = 0; i < 10; ++i) { dfs(u * 10 + i, n, ans); } } }
class Solution { public List<Integer> lexicalOrder(int n) { List<Integer> ans = new ArrayList<>(); int v = 1; for (int i = 0; i < n; ++i) { ans.add(v); if (v * 10 <= n) { v *= 10; } else { while (v % 10 == 9 || v + 1 > n) { v /= 10; } ++v; } } return ans; } }
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
示例 1:
输入: s = "leetcode" 输出: 0
示例 2:
输入: s = "loveleetcode" 输出: 2
示例 3:
输入: s = "aabb" 输出: -1
提示:
1 <= s.length <= 105s 只包含小写字母方法一:数组或哈希表
我们可以用数组或哈希表 记录字符串 中每个字符出现的次数。
然后我们再遍历字符串 ,当遍历到某个字符 时,如果 ,则说明 是第一个不重复的字符,返回它的索引即可。
如果遍历完字符串 仍然没有找到不重复的字符,返回 。
时间复杂度 ,空间复杂度 ,其中 是字符集的大小。
class Solution { public int firstUniqChar(String s) { int[] cnt = new int[26]; int n = s.length(); for (int i = 0; i < n; ++i) { ++cnt[s.charAt(i) - 'a']; } for (int i = 0; i < n; ++i) { if (cnt[s.charAt(i) - 'a'] == 1) { return i; } } return -1; } }
假设有一个同时存储文件和目录的文件系统。下图展示了文件系统的一个示例:

这里将 dir 作为根目录中的唯一目录。dir 包含两个子目录 subdir1 和 subdir2 。subdir1 包含文件 file1.ext 和子目录 subsubdir1;subdir2 包含子目录 subsubdir2,该子目录下包含文件 file2.ext 。
在文本格式中,如下所示(⟶表示制表符):
dir ⟶ subdir1 ⟶ ⟶ file1.ext ⟶ ⟶ subsubdir1 ⟶ subdir2 ⟶ ⟶ subsubdir2 ⟶ ⟶ ⟶ file2.ext
如果是代码表示,上面的文件系统可以写为 "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext" 。'\n' 和 '\t' 分别是换行符和制表符。
文件系统中的每个文件和文件夹都有一个唯一的 绝对路径 ,即必须打开才能到达文件/目录所在位置的目录顺序,所有路径用 '/' 连接。上面例子中,指向 file2.ext 的 绝对路径 是 "dir/subdir2/subsubdir2/file2.ext" 。每个目录名由字母、数字和/或空格组成,每个文件名遵循 name.extension 的格式,其中 name 和 extension由字母、数字和/或空格组成。
给定一个以上述格式表示文件系统的字符串 input ,返回文件系统中 指向 文件 的 最长绝对路径 的长度 。 如果系统中没有文件,返回 0。
示例 1:
输入:input = "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext" 输出:20 解释:只有一个文件,绝对路径为 "dir/subdir2/file.ext" ,路径长度 20
示例 2:
输入:input = "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext" 输出:32 解释:存在两个文件: "dir/subdir1/file1.ext" ,路径长度 21 "dir/subdir2/subsubdir2/file2.ext" ,路径长度 32 返回 32 ,因为这是最长的路径
示例 3:
输入:input = "a" 输出:0 解释:不存在任何文件
示例 4:
输入:input = "file1.txt\nfile2.txt\nlongfile.txt" 输出:12 解释:根目录下有 3 个文件。 因为根目录中任何东西的绝对路径只是名称本身,所以答案是 "longfile.txt" ,路径长度为 12
提示:
1 <= input.length <= 104input 可能包含小写或大写的英文字母,一个换行符 '\n',一个制表符 '\t',一个点 '.',一个空格 ' ',和数字。遍历文件系统的时候需要在各个目录间切换,在实际的 Linux 中,有 pushd 和 popd 命令,本题可以使用栈模拟这一过程
class Solution { public int lengthLongestPath(String input) { int i = 0; int n = input.length(); int ans = 0; Deque<Integer> stack = new ArrayDeque<>(); while (i < n) { int ident = 0; for (; input.charAt(i) == '\t'; i++) { ident++; } int cur = 0; boolean isFile = false; for (; i < n && input.charAt(i) != '\n'; i++) { cur++; if (input.charAt(i) == '.') { isFile = true; } } i++; // popd while (!stack.isEmpty() && stack.size() > ident) { stack.pop(); } if (stack.size() > 0) { cur += stack.peek() + 1; } // pushd if (!isFile) { stack.push(cur); continue; } ans = Math.max(ans, cur); } return ans; } }
给定两个字符串 s 和 t ,它们只包含小写字母。
字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。
请找出在 t 中被添加的字母。
示例 1:
输入:s = "abcd", t = "abcde" 输出:"e" 解释:'e' 是那个被添加的字母。
示例 2:
输入:s = "", t = "y" 输出:"y"
提示:
0 <= s.length <= 1000t.length == s.length + 1s 和 t 只包含小写字母方法一:计数
使用数组(cnt)统计 s 与 t 当中字符出现的次数:s[i] 进行 cnt[s[i] - 'a']++,t[i] 进行 cnt[t[i] - 'a']--。
完成统计后,找到符合 cnt[i] == -1 的 i,返回即可(return 'a' + i)。
时间复杂度 ,空间复杂度 。本题中 。
方法二:求和
由于 s 与 t 只存在一个不同元素,可以统计两者所有字符 ASCII 码之和,再进行相减(sum(t) - sum(s)),即可得到 t 中那一个额外字符的 ASCII 码。
时间复杂度 ,空间复杂度 。
class Solution { public char findTheDifference(String s, String t) { int[] cnt = new int[26]; for (int i = 0; i < s.length(); ++i) { ++cnt[s.charAt(i) - 'a']; } for (int i = 0; ; ++i) { if (--cnt[t.charAt(i) - 'a'] < 0) { return t.charAt(i); } } } }
class Solution { public char findTheDifference(String s, String t) { int ss = 0; for (int i = 0; i < t.length(); ++i) { ss += t.charAt(i); } for (int i = 0; i < s.length(); ++i) { ss -= s.charAt(i); } return (char) ss; } }
列表 arr 由在范围 [1, n] 中的所有整数组成,并按严格递增排序。请你对 arr 应用下述算法:
给你整数 n ,返回 arr 最后剩下的数字。
示例 1:
输入:n = 9 输出:6 解释: arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] arr = [2, 4, 6, 8] arr = [2, 6] arr = [6]
示例 2:
输入:n = 1 输出:1
提示:
1 <= n <= 109用 i 记录该从左边还是右边进行删除。由于经过每轮删除过后都是一个等差数列,因此我们用 a1, an 记录首尾元素,cnt 记录数列元素个数,step 记录元素间的间隔,step 初始为 1。
a1 变为 a1 + stepcnt 为奇数个,an 变为 an - step,否则 an 不变an 变为 an - stepcnt 为奇数个,a1 变为 a1 + step,否则 a1 不变每次经过一轮删除,数列元素个数 cnt 变为 cnt >> 1,元素间隔 step 变为 step << 1,i 自增 1。
当元素个数剩下 1 个时,退出循环,返回 a1 即可。
class Solution { public int lastRemaining(int n) { int a1 = 1, an = n, step = 1; for (int i = 0, cnt = n; cnt > 1; cnt >>= 1, step <<= 1, ++i) { if (i % 2 == 1) { an -= step; if (cnt % 2 == 1) { a1 += step; } } else { a1 += step; if (cnt % 2 == 1) { an -= step; } } } return a1; } }
给你一个数组 rectangles ,其中 rectangles[i] = [xi, yi, ai, bi] 表示一个坐标轴平行的矩形。这个矩形的左下顶点是 (xi, yi) ,右上顶点是 (ai, bi) 。
如果所有矩形一起精确覆盖了某个矩形区域,则返回 true ;否则,返回 false 。
示例 1:
输入:rectangles = [[1,1,3,3],[3,1,4,2],[3,2,4,4],[1,3,2,4],[2,3,3,4]] 输出:true 解释:5 个矩形一起可以精确地覆盖一个矩形区域。
示例 2:
输入:rectangles = [[1,1,2,3],[1,3,2,4],[3,1,4,2],[3,2,4,4]] 输出:false 解释:两个矩形之间有间隔,无法覆盖成一个矩形。
示例 3:
输入:rectangles = [[1,1,3,3],[3,1,4,2],[1,3,2,4],[2,2,4,4]] 输出:false 解释:因为中间有相交区域,虽然形成了矩形,但不是精确覆盖。
提示:
1 <= rectangles.length <= 2 * 104rectangles[i].length == 4-105 <= xi, yi, ai, bi <= 105利用哈希表统计小矩形顶点出现的次数,除了最终大矩形的四个顶点只出现 1 次外,其他顶点的出现次数只有可能是 2 或 4。另外,为了满足条件,小矩形的面积和必须等于大矩形(无重叠)
class Solution { public boolean isRectangleCover(int[][] rectangles) { long area = 0; int minX = rectangles[0][0], minY = rectangles[0][1]; int maxX = rectangles[0][2], maxY = rectangles[0][3]; Map<Pair, Integer> cnt = new HashMap<>(); for (int[] r : rectangles) { area += (r[2] - r[0]) * (r[3] - r[1]); minX = Math.min(minX, r[0]); minY = Math.min(minY, r[1]); maxX = Math.max(maxX, r[2]); maxY = Math.max(maxY, r[3]); cnt.merge(new Pair(r[0], r[1]), 1, Integer::sum); cnt.merge(new Pair(r[0], r[3]), 1, Integer::sum); cnt.merge(new Pair(r[2], r[3]), 1, Integer::sum); cnt.merge(new Pair(r[2], r[1]), 1, Integer::sum); } if (area != (long) (maxX - minX) * (maxY - minY) || cnt.getOrDefault(new Pair(minX, minY), 0) != 1 || cnt.getOrDefault(new Pair(minX, maxY), 0) != 1 || cnt.getOrDefault(new Pair(maxX, maxY), 0) != 1 || cnt.getOrDefault(new Pair(maxX, minY), 0) != 1) { return false; } cnt.remove(new Pair(minX, minY)); cnt.remove(new Pair(minX, maxY)); cnt.remove(new Pair(maxX, maxY)); cnt.remove(new Pair(maxX, minY)); return cnt.values().stream().allMatch(c -> c == 2 || c == 4); } private static class Pair { final int first; final int second; Pair(int first, int second) { this.first = first; this.second = second; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Pair pair = (Pair) o; return first == pair.first && second == pair.second; } @Override public int hashCode() { return Objects.hash(first, second); } } }
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
致谢:
特别感谢 @pbrother 添加此问题并且创建所有测试用例。
示例 1:
输入:s = "abc", t = "ahbgdc" 输出:true
示例 2:
输入:s = "axc", t = "ahbgdc" 输出:false
提示:
0 <= s.length <= 1000 <= t.length <= 10^4方法一:双指针
我们定义两个指针 和 ,分别指向字符串 和 的初始位置。每次我们比较两个指针指向的字符,如果相同,则两个指针同时右移;如果不同,则只有 右移。当指针 移动到字符串 的末尾时,说明 是 的子序列。
时间复杂度 ,其中 和 分别是字符串 和 的长度。空间复杂度 。
class Solution { public boolean isSubsequence(String s, String t) { int m = s.length(), n = t.length(); int i = 0, j = 0; while (i < m && j < n) { if (s.charAt(i) == t.charAt(j)) { ++i; } ++j; } return i == m; } }
给定一个表示数据的整数数组 data ,返回它是否为有效的 UTF-8 编码。
UTF-8 中的一个字符可能的长度为 1 到 4 字节,遵循以下的规则:
这是 UTF-8 编码的工作方式:
Number of Bytes | UTF-8 octet sequence
| (binary)
--------------------+---------------------------------------------
1 | 0xxxxxxx
2 | 110xxxxx 10xxxxxx
3 | 1110xxxx 10xxxxxx 10xxxxxx
4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
x 表示二进制形式的一位,可以是 0 或 1。
注意:输入是整数数组。只有每个整数的 最低 8 个有效位 用来存储数据。这意味着每个整数只表示 1 字节的数据。
示例 1:
输入:data = [197,130,1] 输出:true 解释:数据表示字节序列:11000101 10000010 00000001。 这是有效的 utf-8 编码,为一个 2 字节字符,跟着一个 1 字节字符。
示例 2:
输入:data = [235,140,4] 输出:false 解释:数据表示 8 位的序列: 11101011 10001100 00000100. 前 3 位都是 1 ,第 4 位为 0 表示它是一个 3 字节字符。 下一个字节是开头为 10 的延续字节,这是正确的。 但第二个延续字节不以 10 开头,所以是不符合规则的。
提示:
1 <= data.length <= 2 * 1040 <= data[i] <= 255class Solution { public boolean validUtf8(int[] data) { int n = 0; for (int v : data) { if (n > 0) { if (v >> 6 != 0b10) { return false; } --n; } else if (v >> 7 == 0) { n = 0; } else if (v >> 5 == 0b110) { n = 1; } else if (v >> 4 == 0b1110) { n = 2; } else if (v >> 3 == 0b11110) { n = 3; } else { return false; } } return n == 0; } }
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = "3[a]2[bc]" 输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]" 输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef" 输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz" 输出:"abccdcdcdxyz"
提示:
1 <= s.length <= 30s 由小写英文字母、数字和方括号 '[]' 组成s 保证是一个 有效 的输入。s 中所有整数的取值范围为 [1, 300] 用栈 s1 存储左括号前的数字 num,栈 s2 存储左括号前的字符串 res。
遍历字符串 s 中每个字符 c:
c == '[',则将左括号前的数字 num 存入 s1,左括号前的字符串 res 存入 s2,并将 num 重新置为 0,res 置为空串c == ']',则 res = s2.pop() + res * s1.pop()最后返回 res 即可。
class Solution { public String decodeString(String s) { Deque<Integer> s1 = new ArrayDeque<>(); Deque<String> s2 = new ArrayDeque<>(); int num = 0; String res = ""; for (char c : s.toCharArray()) { if ('0' <= c && c <= '9') { num = num * 10 + c - '0'; } else if (c == '[') { s1.push(num); s2.push(res); num = 0; res = ""; } else if (c == ']') { StringBuilder t = new StringBuilder(); for (int i = 0, n = s1.pop(); i < n; ++i) { t.append(res); } res = s2.pop() + t.toString(); } else { res += String.valueOf(c); } } return res; } }
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
示例 1:
输入:s = "aaabb", k = 3 输出:3 解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
示例 2:
输入:s = "ababbc", k = 2 输出:5 解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。
提示:
1 <= s.length <= 104s 仅由小写英文字母组成1 <= k <= 105方法一:分治
对于字符串 ,如果存在某个字符 ,其出现次数小于 ,则任何包含 的子串都不可能满足题目要求。因此我们可以将 按照每个不满足条件的字符 进行分割,分割得到的每个子串都是原字符串的一个「子问题」,我们可以递归地求解每个子问题,最终的答案即为所有子问题的最大值。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度,而 为字符集的大小。本题中 。
class Solution { private String s; private int k; public int longestSubstring(String s, int k) { this.s = s; this.k = k; return dfs(0, s.length() - 1); } private int dfs(int l, int r) { int[] cnt = new int[26]; for (int i = l; i <= r; ++i) { ++cnt[s.charAt(i) - 'a']; } char split = 0; for (int i = 0; i < 26; ++i) { if (cnt[i] > 0 && cnt[i] < k) { split = (char) (i + 'a'); break; } } if (split == 0) { return r - l + 1; } int i = l; int ans = 0; while (i <= r) { while (i <= r && s.charAt(i) == split) { ++i; } if (i > r) { break; } int j = i; while (j <= r && s.charAt(j) != split) { ++j; } int t = dfs(i, j - 1); ans = Math.max(ans, t); i = j; } return ans; } }
给定一个长度为 n 的整数数组 nums 。
假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数 F 为:
F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]返回 F(0), F(1), ..., F(n-1)中的最大值 。
生成的测试用例让答案符合 32 位 整数。
示例 1:
输入: nums = [4,3,2,6] 输出: 26 解释: F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25 F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16 F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23 F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。
示例 2:
输入: nums = [100] 输出: 0
提示:
n == nums.length1 <= n <= 105-100 <= nums[i] <= 100f(0) = 0 * nums[0] + 1 * nums[1] + ... + (n - 1) * nums[n - 1]
f(1) = 1 * nums[0] + 2 * nums[1] + ... + 0 * nums[n - 1]
...
f(k) = f(k - 1) + s - n * nums[n - k]
class Solution { public int maxRotateFunction(int[] nums) { int f = 0; int s = 0; int n = nums.length; for (int i = 0; i < n; ++i) { f += i * nums[i]; s += nums[i]; } int ans = f; for (int i = 1; i < n; ++i) { f = f + s - n * nums[n - i]; ans = Math.max(ans, f); } return ans; } }
给定一个正整数 n ,你可以做如下操作:
n 是偶数,则用 n / 2替换 n 。n 是奇数,则可以用 n + 1或n - 1替换 n 。返回 n 变为 1 所需的 最小替换次数 。
示例 1:
输入:n = 8 输出:3 解释:8 -> 4 -> 2 -> 1
示例 2:
输入:n = 7 输出:4 解释:7 -> 8 -> 4 -> 2 -> 1 或 7 -> 6 -> 3 -> 2 -> 1
示例 3:
输入:n = 4 输出:2
提示:
1 <= n <= 231 - 1偶数直接除以 2,对于奇数,若二进制形式如 0bxxx11,并且不为 3,则进行加 1,否则进行减 1。
class Solution { public int integerReplacement(int n) { int ans = 0; while (n != 1) { if ((n & 1) == 0) { n >>>= 1; } else if (n != 3 && (n & 3) == 3) { ++n; } else { --n; } ++ans; } return ans; } }
给你一个可能含有 重复元素 的整数数组 nums ,请你随机输出给定的目标数字 target 的索引。你可以假设给定的数字一定存在于数组中。
实现 Solution 类:
Solution(int[] nums) 用数组 nums 初始化对象。int pick(int target) 从 nums 中选出一个满足 nums[i] == target 的随机索引 i 。如果存在多个有效的索引,则每个索引的返回概率应当相等。示例:
输入 ["Solution", "pick", "pick", "pick"] [[[1, 2, 3, 3, 3]], [3], [1], [3]] 输出 [null, 4, 0, 2] 解释 Solution solution = new Solution([1, 2, 3, 3, 3]); solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。 solution.pick(1); // 返回 0 。因为只有 nums[0] 等于 1 。 solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。
提示:
1 <= nums.length <= 2 * 104-231 <= nums[i] <= 231 - 1target 是 nums 中的一个整数pick 函数 104 次蓄水池抽样问题。即从一个包含 n 个对象的列表 S 中随机选取 k 个对象,n 为一个非常大或者不知道的值。通常情况下,n 是一个非常大的值,大到无法一次性把所有列表 S 中的对象都放到内存中。我们这个问题是蓄水池抽样问题的一个特例,即 k=1。
解法:我们总是选择第一个对象,以 1/2 的概率选择第二个,以 1/3 的概率选择第三个,以此类推,以 1/m 的概率选择第 m 个对象。当该过程结束时,每一个对象具有相同的选中概率,即 1/n。
证明:第 m 个对象最终被选中的概率 P = 选择 m 的概率 × 其后面所有对象不被选择的概率,即:
思路同:382. 链表随机节点
class Solution { private int[] nums; private Random random = new Random(); public Solution(int[] nums) { this.nums = nums; } public int pick(int target) { int n = 0, ans = 0; for (int i = 0; i < nums.length; ++i) { if (nums[i] == target) { ++n; int x = 1 + random.nextInt(n); if (x == n) { ans = i; } } } return ans; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(nums); * int param_1 = obj.pick(target); */
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
示例 1:
输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] 输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000] 解释: 条件:a / b = 2.0, b / c = 3.0 问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? 结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]
示例 2:
输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] 输出:[3.75000,0.40000,5.00000,0.20000]
示例 3:
输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]] 输出:[0.50000,2.00000,-1.00000,-1.00000]
提示:
1 <= equations.length <= 20equations[i].length == 21 <= Ai.length, Bi.length <= 5values.length == equations.length0.0 < values[i] <= 20.01 <= queries.length <= 20queries[i].length == 21 <= Cj.length, Dj.length <= 5Ai, Bi, Cj, Dj 由小写英文字母与数字组成并查集。对于本题,具备等式关系的所有变量构成同一个集合,同时,需要维护一个权重数组 w,初始时 w[i] = 1。对于等式关系如 a / b = 2,令 w[a] = 2。在 find() 查找祖宗节点的时候,同时进行路径压缩,并更新节点权重。而在合并节点时,p[pa] = pb,同时更新 pa 的权重为 w[pa] = w[b] * (a / b) / w[a]。
以下是并查集的几个常用模板。
模板 1——朴素并查集:
模板 2——维护 size 的并查集:
模板 3——维护到祖宗节点距离的并查集:
class Solution { private Map<String, String> p; private Map<String, Double> w; public double[] calcEquation( List<List<String>> equations, double[] values, List<List<String>> queries) { int n = equations.size(); p = new HashMap<>(); w = new HashMap<>(); for (List<String> e : equations) { p.put(e.get(0), e.get(0)); p.put(e.get(1), e.get(1)); w.put(e.get(0), 1.0); w.put(e.get(1), 1.0); } for (int i = 0; i < n; ++i) { List<String> e = equations.get(i); String a = e.get(0), b = e.get(1); String pa = find(a), pb = find(b); if (Objects.equals(pa, pb)) { continue; } p.put(pa, pb); w.put(pa, w.get(b) * values[i] / w.get(a)); } int m = queries.size(); double[] ans = new double[m]; for (int i = 0; i < m; ++i) { String c = queries.get(i).get(0), d = queries.get(i).get(1); ans[i] = !p.containsKey(c) || !p.containsKey(d) || !Objects.equals(find(c), find(d)) ? -1.0 : w.get(c) / w.get(d); } return ans; } private String find(String x) { if (!Objects.equals(p.get(x), x)) { String origin = p.get(x); p.put(x, find(p.get(x))); w.put(x, w.get(x) * w.get(origin)); } return p.get(x); } }
给你一个整数 n ,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...] 中找出并返回第 n 位上的数字。
示例 1:
输入:n = 3 输出:3
示例 2:
输入:n = 11 输出:0 解释:第 11 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 0 ,它是 10 的一部分。
提示:
1 <= n <= 231 - 1方法一:数学
位数为 的最小整数和最大整数分别为 和 ,因此 位数的总位数为 。
我们用 表示当前数字的位数,用 表示当前位数的数字的总数,初始时 , 。
每次将 减去 ,当 小于等于 时,说明 对应的数字在当前位数的数字范围内,此时可以计算出对应的数字。
具体做法是,首先计算出 对应的是当前位数的哪一个数字,然后计算出是该数字的第几位,从而得到该位上的数字。
时间复杂度 。
class Solution { public int findNthDigit(int n) { int k = 1, cnt = 9; while ((long) k * cnt < n) { n -= k * cnt; ++k; cnt *= 10; } int num = (int) Math.pow(10, k - 1) + (n - 1) / k; int idx = (n - 1) % k; return String.valueOf(num).charAt(idx) - '0'; } }
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
"3:25" 。
(图源:WikiMedia - Binary clock samui moon.jpg ,许可协议:Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) )
给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。
小时不会以零开头:
"01:00" 是无效的时间,正确的写法应该是 "1:00" 。分钟必须由两位数组成,可能会以零开头:
"10:2" 是无效的时间,正确的写法应该是 "10:02" 。示例 1:
输入:turnedOn = 1 输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"]
示例 2:
输入:turnedOn = 9 输出:[]
提示:
0 <= turnedOn <= 10方法一:枚举组合
题目可转换为求 i(i∈[0,12)) 和 j(j∈[0,60)) 所有可能的组合。
合法组合需要满足的条件是:i 的二进制形式中 1 的个数加上 j 的二进制形式中 1 的个数,结果等于 turnedOn。
方法二:二进制枚举
利用 10 个二进制位表示手表,其中前 4 位代表小时,后 6 位代表分钟。枚举 [0, 1 << 10) 的所有数,找出合法的数。
class Solution { public List<String> readBinaryWatch(int turnedOn) { List<String> ans = new ArrayList<>(); for (int i = 0; i < 12; ++i) { for (int j = 0; j < 60; ++j) { if (Integer.bitCount(i) + Integer.bitCount(j) == turnedOn) { ans.add(String.format("%d:%02d", i, j)); } } } return ans; } }
class Solution { public List<String> readBinaryWatch(int turnedOn) { List<String> ans = new ArrayList<>(); for (int i = 0; i < 1 << 10; ++i) { int h = i >> 6, m = i & 0b111111; if (h < 12 && m < 60 && Integer.bitCount(i) == turnedOn) { ans.add(String.format("%d:%02d", h, m)); } } return ans; } }
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = "1432219", k = 3 输出:"1219" 解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
示例 2 :
输入:num = "10200", k = 1 输出:"200" 解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :
输入:num = "10", k = 2 输出:"0" 解释:从原数字移除所有的数字,剩余为空就是 0 。
提示:
1 <= k <= num.length <= 105num 仅由若干位数字(0 - 9)组成num 不含任何前导零方法一:贪心算法
前置知识:两个相同位数的数字大小关系取决于第一个不同位的数的大小。
基本的思路如下:
时间复杂度 ,空间复杂度 。
class Solution { public String removeKdigits(String num, int k) { StringBuilder stk = new StringBuilder(); for (char c : num.toCharArray()) { while (k > 0 && stk.length() > 0 && stk.charAt(stk.length() - 1) > c) { stk.deleteCharAt(stk.length() - 1); --k; } stk.append(c); } for (; k > 0; --k) { stk.deleteCharAt(stk.length() - 1); } int i = 0; for (; i < stk.length() && stk.charAt(i) == '0'; ++i) { } String ans = stk.substring(i); return "".equals(ans) ? "0" : ans; } }
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格 1 跳至单元格 2 )。
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
示例 1:
输入:stones = [0,1,3,5,6,8,12,17] 输出:true 解释:青蛙可以成功过河,按照如下方案跳跃:跳 1 个单位到第 2 块石子, 然后跳 2 个单位到第 3 块石子, 接着 跳 2 个单位到第 4 块石子, 然后跳 3 个单位到第 6 块石子, 跳 4 个单位到第 7 块石子, 最后,跳 5 个单位到第 8 个石子(即最后一块石子)。
示例 2:
输入:stones = [0,1,2,3,4,8,9,11] 输出:false 解释:这是因为第 5 和第 6 个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。
提示:
2 <= stones.length <= 20000 <= stones[i] <= 231 - 1stones[0] == 0stones 按严格升序排列方法一:哈希表 + 记忆化搜索
我们用哈希表 记录每个石子的下标,接下来设计一个函数 ,表示青蛙从第 个石子跳跃且上一次跳跃距离为 ,如果青蛙能够到达终点,那么函数返回 true,否则返回 false。
函数 的计算过程如下:
如果 是最后一个石子的下标,那么青蛙已经到达终点,返回 true;
否则,我们需要枚举青蛙接下来的跳跃距离 ,其中 。如果 是正数,并且哈希表 中存在位置 ,那么青蛙在第 个石子上可以选择跳跃 个单位,如果 返回 true,那么青蛙可以从第 个石子成功跳跃到终点,我们就可以返回 true。
枚举结束,说明青蛙在第 个石子上无法选择合适的跳跃距离跳到终点,我们就返回 false。
为了防止函数 中出现重复计算,我们可以使用记忆化搜索,将 的结果记录在一个数组 中,每当函数 返回结果,我们就将 进行赋值,并在下次遇到 时直接返回 。
时间复杂度 ,空间复杂度 。其中 是石子的数量。
方法二:动态规划
我们定义 表示青蛙能否达到「现在所处的石子编号」为 ,「上一次跳跃距离」为 的状态。初始时 ,其余均为 false。
考虑 ,我们可以枚举上一块石子的编号 ,那么上一次跳跃的距离 。如果 ,那么青蛙无法从第 块石子跳跃到第 块石子,我们可以直接跳过这种情况。否则,青蛙可以从第 块石子跳跃到第 块石子,那么 。如果 ,且 ,那么青蛙可以成功过河,我们就可以返回 true。
否则,我们最后返回 false。
时间复杂度 ,空间复杂度 。其中 是石子的数量。
class Solution { private Boolean[][] f; private Map<Integer, Integer> pos = new HashMap<>(); private int[] stones; private int n; public boolean canCross(int[] stones) { n = stones.length; f = new Boolean[n][n]; this.stones = stones; for (int i = 0; i < n; ++i) { pos.put(stones[i], i); } return dfs(0, 0); } private boolean dfs(int i, int k) { if (i == n - 1) { return true; } if (f[i][k] != null) { return f[i][k]; } for (int j = k - 1; j <= k + 1; ++j) { if (j > 0) { int h = stones[i] + j; if (pos.containsKey(h) && dfs(pos.get(h), j)) { return f[i][k] = true; } } } return f[i][k] = false; } }
class Solution { public boolean canCross(int[] stones) { int n = stones.length; boolean[][] f = new boolean[n][n]; f[0][0] = true; for (int i = 1; i < n; ++i) { for (int j = i - 1; j >= 0; --j) { int k = stones[i] - stones[j]; if (k - 1 > j) { break; } f[i][k] = f[j][k - 1] || f[j][k] || f[j][k + 1]; if (i == n - 1 && f[i][k]) { return true; } } } return false; } }
给定二叉树的根节点 root ,返回所有左叶子之和。
示例 1:

输入: root = [3,9,20,null,null,15,7] 输出: 24 解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
示例 2:
输入: root = [1] 输出: 0
提示:
[1, 1000] 范围内-1000 <= Node.val <= 1000class Solution { public int sumOfLeftLeaves(TreeNode root) { if (root == null) { return 0; } int res = 0; if (root.left != null && root.left.left == null && root.left.right == null) { res += root.left.val; } res += sumOfLeftLeaves(root.left); res += sumOfLeftLeaves(root.right); return res; } }
给定一个整数,编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用 补码运算 方法。
注意:
a-f)都必须是小写。'0'来表示;对于其他情况,十六进制字符串中的第一个字符将不会是0字符。 示例 1:
输入: 26 输出: "1a"
示例 2:
输入: -1 输出: "ffffffff"
将数字的二进制位每 4 个一组转换为 16 进制即可。
class Solution { public String toHex(int num) { if (num == 0) { return "0"; } StringBuilder sb = new StringBuilder(); while (num != 0) { int x = num & 15; if (x < 10) { sb.append(x); } else { sb.append((char) (x - 10 + 'a')); } num >>>= 4; } return sb.reverse().toString(); } }
class Solution { public String toHex(int num) { if (num == 0) { return "0"; } StringBuilder sb = new StringBuilder(); for (int i = 7; i >= 0; --i) { int x = (num >> (4 * i)) & 0xf; if (sb.length() > 0 || x != 0) { char c = x < 10 ? (char) (x + '0') : (char) (x - 10 + 'a'); sb.append(c); } } return sb.toString(); } }
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 解释: 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
1 <= people.length <= 20000 <= hi <= 1060 <= ki < people.length对 people 按照身高降序排列,若身高相同,则按照人数 k 升序排列。然后按照索引位置依次将 people 插入到结果列表中即可。
class Solution { public int[][] reconstructQueue(int[][] people) { Arrays.sort(people, (a, b) -> a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]); List<int[]> ans = new ArrayList<>(people.length); for (int[] p : people) { ans.add(p[1], p); } return ans.toArray(new int[ans.size()][]); } }
给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例 1:

输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] 输出: 4 解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。
示例 2:

输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]] 输出: 10
提示:
m == heightMap.lengthn == heightMap[i].length1 <= m, n <= 2000 <= heightMap[i][j] <= 2 * 104方法一:优先队列(小根堆)
接雨水问题的变种,由于矩阵的边界上的高度是确定的,因此可以将矩阵的边界上的高度加入优先队列,然后从优先队列中取出最小的高度,然后将其四周的高度与其比较,如果四周的高度小于当前高度,则可以接雨水,接雨水的体积为当前高度减去四周的高度,然后将较大的高度加入优先队列,重复上述过程,直到优先队列为空。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵的行数和列数。
class Solution { public int trapRainWater(int[][] heightMap) { int m = heightMap.length, n = heightMap[0].length; boolean[][] vis = new boolean[m][n]; PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (i == 0 || i == m - 1 || j == 0 || j == n - 1) { pq.offer(new int[] {heightMap[i][j], i, j}); vis[i][j] = true; } } } int ans = 0; int[] dirs = {-1, 0, 1, 0, -1}; while (!pq.isEmpty()) { var p = pq.poll(); for (int k = 0; k < 4; ++k) { int x = p[1] + dirs[k], y = p[2] + dirs[k + 1]; if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y]) { ans += Math.max(0, p[0] - heightMap[x][y]); vis[x][y] = true; pq.offer(new int[] {Math.max(p[0], heightMap[x][y]), x, y}); } } } return ans; } }
字符串可以用 缩写 进行表示,缩写 的方法是将任意数量的 不相邻 的子字符串替换为相应子串的长度。例如,字符串 "substitution" 可以缩写为(不止这几种方法):
"s10n" ("s ubstitutio n")"sub4u4" ("sub stit u tion")"12" ("substitution")"su3i1u2on" ("su bst i t u ti on")"substitution" (没有替换子字符串)下列是不合法的缩写:
"s55n" ("s ubsti tutio n",两处缩写相邻)"s010n" (缩写存在前导零)"s0ubstitution" (缩写是一个空字符串)给你一个字符串单词 word 和一个缩写 abbr ,判断这个缩写是否可以是给定单词的缩写。
子字符串是字符串中连续的非空字符序列。
示例 1:
输入:word = "internationalization", abbr = "i12iz4n"
输出:true
解释:单词 "internationalization" 可以缩写为 "i12iz4n" ("i nternational iz atio n") 。
示例 2:
输入:word = "apple", abbr = "a2e" 输出:false 解释:单词 "apple" 无法缩写为 "a2e" 。
提示:
1 <= word.length <= 20word 仅由小写英文字母组成1 <= abbr.length <= 10abbr 由小写英文字母和数字组成abbr 中的所有数字均符合 32-bit 整数范围方法一:模拟
模拟字符匹配替换。
同时遍历 和 ,若 遇到数字,则 跳过对应数字长度的字符数。若数字为空,或者有前导零,则提前返回 false。
时间复杂度 ,空间复杂度 。其中 是 的长度,而 是 的长度。
class Solution { public boolean validWordAbbreviation(String word, String abbr) { int m = word.length(), n = abbr.length(); int i = 0, j = 0; while (i < m) { if (j >= n) { return false; } if (word.charAt(i) == abbr.charAt(j)) { ++i; ++j; continue; } int k = j; while (k < n && Character.isDigit(abbr.charAt(k))) { ++k; } String t = abbr.substring(j, k); if (j == k || t.charAt(0) == '0' || Integer.parseInt(t) == 0) { return false; } i += Integer.parseInt(t); j = k; } return i == m && j == n; } }
给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串 。
在构造过程中,请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。
示例 1:
输入:s = "abccccdd" 输出:7 解释: 我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
示例 2:
输入:s = "a" 输出:1
示例 3:
输入:s = "aaaaaccc" 输出:7
提示:
1 <= s.length <= 2000s 只由小写 和/或 大写英文字母组成方法一:计数
一个合法的回文字符串,最多存在一个出现奇数次数的字符,其余字符出现次数均为偶数。
因此,我们可以先遍历字符串 ,统计每个字符出现的次数,记录在数组或哈希表 中。
然后,我们遍历 ,对于每个字符 ,如果 为偶数,则直接将 累加到答案 中;如果 为奇数,则将 累加到 中,如果 为偶数,则将 增加 。
最后,我们返回 即可。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度;而 为字符集的大小,本题中 。
class Solution { public int longestPalindrome(String s) { int[] cnt = new int[128]; for (int i = 0; i < s.length(); ++i) { ++cnt[s.charAt(i)]; } int ans = 0; for (int v : cnt) { ans += v - (v & 1); if (ans % 2 == 0 && v % 2 == 1) { ++ans; } } return ans; } }
给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。
设计一个算法使得这 m 个子数组各自和的最大值最小。
示例 1:
输入:nums = [7,2,5,10,8], m = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
示例 2:
输入:nums = [1,2,3,4,5], m = 2 输出:9
示例 3:
输入:nums = [1,4,4], m = 3 输出:4
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1061 <= m <= min(50, nums.length)方法一:二分查找
我们注意到,当子数组的和的最大值越大,子数组的个数越少,当存在一个满足条件的子数组和的最大值时,那么比这个最大值更大的子数组和的最大值一定也满足条件。也就是说,我们可以对子数组和的最大值进行二分查找,找到满足条件的最小值。
我们定义二分查找的左边界 ,右边界 ,然后对于二分查找的每一步,我们取中间值 ,然后判断是否存在一个分割方式,使得子数组的和的最大值不超过 ,如果存在,则说明 可能是满足条件的最小值,因此我们将右边界调整为 ,否则我们将左边界调整为 。
我们如何判断是否存在一个分割方式,使得子数组的和的最大值不超过 呢?我们可以使用贪心的方法,从左到右遍历数组,将数组中的元素依次加入到子数组中,如果当前子数组的和大于 ,则我们将当前元素加入到下一个子数组中。如果我们能够将数组分割成不超过 个子数组,且每个子数组的和的最大值不超过 ,则说明 是满足条件的最小值,否则 不是满足条件的最小值。
时间复杂度 。其中 和 分别是数组的长度和数组所有元素的和。
class Solution { public int splitArray(int[] nums, int k) { int left = 0, right = 0; for (int x : nums) { left = Math.max(left, x); right += x; } while (left < right) { int mid = (left + right) >> 1; if (check(nums, mid, k)) { right = mid; } else { left = mid + 1; } } return left; } private boolean check(int[] nums, int mx, int k) { int s = 1 << 30, cnt = 0; for (int x : nums) { s += x; if (s > mx) { ++cnt; s = x; } } return cnt <= k; } }
通过将任意数量的 不相邻 子字符串替换为它们的长度,可以完成对字符串的 缩写 。 例如,像 "substitution" 这样的字符串可以缩写为(但不限于):
"s10n" ("s ubstitutio n")"sub4u4" ("sub stit u tion")"12" ("substitution")"su3i1u2on" ("su bst i t u ti on")"substitution" (不替换子字符串)注意:"s55n" ("s ubsti tutio n") 不是 "substitution" 的有效缩写形式,因为它试图替换两个相邻的子字符串。
缩写的 长度 是未被替换的字母数加上被替换的字符串数。例如,缩写 "s10n" 的长度为 3(2 个字母 + 1 个子字符串),而 "su3i1u2on" 的长度为 9(6 个字母 + 3 子字符串)。
给你一个目标字符串 target 和一个字符串数组 dictionary 作为字典,为 target 找出并返回一个 最短 长度的缩写字符串,同时这个缩写字符串 不是 字典 dictionary 中其他字符串的缩写形式。如果有多个有效答案,可以返回其中任意一个。
示例 1:
输入:target = "apple", dictionary = ["blade"] 输出:"a4" 解释:"apple" 的最短缩写形式为 "5" ,但这也是 "blade" 的缩写形式之一。 下一组最短缩写是 "a4" 和 "4e" ,其中 "4e" 也是 "blade" 的缩写形式之一,而 "a4" 不是。 因此,返回 "a4" 。
示例 2:
输入:target = "apple", dictionary = ["blade","plain","amber"] 输出:"1p3" 解释:"5" 同时是 "apple" 和字典中所有单词的缩写形式。 "a4" 同时是 "apple" 和 "amber" 的缩写形式。 "4e" 同时是 "apple" 和 "blade" 的缩写形式。 "1p3"、"2p2" 和 "3l1" 是 "apple" 的下一组最短缩写形式。 因为它们不是字典中其他单词的缩写形式,返回其中任意一个都是正确的。
提示:
target.length == mdictionary.length == n1 <= m <= 210 <= n <= 10001 <= dictionary[i].length <= 100n > 0 ,那么 log2(n) + m <= 21target 和 dictionary[i] 仅包含小写字符给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 表示,并用字符串数组 answer(下标从 1 开始)返回结果,其中:
answer[i] == "FizzBuzz" 如果 i 同时是 3 和 5 的倍数。answer[i] == "Fizz" 如果 i 是 3 的倍数。answer[i] == "Buzz" 如果 i 是 5 的倍数。answer[i] == i (以字符串形式)如果上述条件全不满足。示例 1:
输入:n = 3 输出:["1","2","Fizz"]
示例 2:
输入:n = 5 输出:["1","2","Fizz","4","Buzz"]
示例 3:
输入:n = 15 输出:["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13","14","FizzBuzz"]
提示:
1 <= n <= 104class Solution { public List<String> fizzBuzz(int n) { List<String> ans = new ArrayList<>(); for (int i = 1; i <= n; ++i) { String s = ""; if (i % 3 == 0) { s += "Fizz"; } if (i % 5 == 0) { s += "Buzz"; } if (s.length() == 0) { s += i; } ans.add(s); } return ans; } }
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
示例 1:
输入:nums = [1,2,3,4] 输出:3 解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
示例 2:
输入:nums = [1] 输出:0
提示:
1 <= nums.length <= 5000-1000 <= nums[i] <= 1000方法一:遍历计数
我们用 表示当前相邻两个元素的差值,用 表示当前等差数列的长度,初始时 , 。
遍历数组 nums,对于相邻的两个元素 和 ,如果 ,则说明当前元素 也属于当前等差数列,此时 自增 1;否则说明当前元素 不属于当前等差数列,此时更新 ,。如果 ,则说明当前等差数列的长度至少为 3,此时等差数列的个数为 ,将其加到答案中。
遍历结束后,即可得到答案。
在代码实现上,我们也可以将 初始化为 ,重置 时,直接将 置为 ;累加答案时,直接累加 即可。
时间复杂度 ,空间复杂度 。其中 是数组 nums 的长度。
相似题目:
class Solution { public int numberOfArithmeticSlices(int[] nums) { int ans = 0, cnt = 0; int d = 3000; for (int i = 0; i < nums.length - 1; ++i) { if (nums[i + 1] - nums[i] == d) { ++cnt; } else { d = nums[i + 1] - nums[i]; cnt = 0; } ans += cnt; } return ans; } }
给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。
示例 1:
输入:[3, 2, 1] 输出:1 解释:第三大的数是 1 。
示例 2:
输入:[1, 2] 输出:2 解释:第三大的数不存在, 所以返回最大的数 2 。
示例 3:
输入:[2, 2, 3, 1] 输出:1 解释:注意,要求返回第三大的数,是指在所有不同数字中排第三大的数。 此例中存在两个值为 2 的数,它们都排第二。在所有不同数字中排第三大的数为 1 。
提示:
1 <= nums.length <= 104-231 <= nums[i] <= 231 - 1进阶:你能设计一个时间复杂度 O(n) 的解决方案吗?
定义 , , 分别表示数组的第 1 大、第 2 大、第 3 大的数,初始化为一个足够小的数。
遍历数组每个元素 num:
num 与前三大数中的某一个相等,直接跳过,因为我们要找的是在所有不同数字中的第三大。num 比 大,说明找到了一个最大的数,此时我们要把 num 赋值给 ,即 ,但在做赋值操作之前,我们要先把旧值赋给 ,依次类推。num 比 、 大的情况,也按照上面的赋值方法进行处理。遍历结束,判断 这个值是否在初始化之后改变过,若是,说明找到了第 3 大数,返回 ,否则返回 。
本方法时间复杂度 ,空间复杂度 。
class Solution { public int thirdMax(int[] nums) { long m1 = Long.MIN_VALUE; long m2 = Long.MIN_VALUE; long m3 = Long.MIN_VALUE; for (int num : nums) { if (num == m1 || num == m2 || num == m3) { continue; } if (num > m1) { m3 = m2; m2 = m1; m1 = num; } else if (num > m2) { m3 = m2; m2 = num; } else if (num > m3) { m3 = num; } } return (int) (m3 != Long.MIN_VALUE ? m3 : m1); } }
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
示例 1:
输入:num1 = "11", num2 = "123" 输出:"134"
示例 2:
输入:num1 = "456", num2 = "77" 输出:"533"
示例 3:
输入:num1 = "0", num2 = "0" 输出:"0"
提示:
1 <= num1.length, num2.length <= 104num1 和num2 都只包含数字 0-9num1 和num2 都不包含任何前导零方法一:双指针
我们用两个指针 和 分别指向两个字符串的末尾,从末尾开始逐位相加。每次取出对应位的数字 和 ,计算它们的和 ,其中 表示上一次相加的进位,最后将 的个位数添加到追加到答案字符串的末尾,然后将 的十位数作为进位 的值,循环此过程直至两个字符串的指针都已经指向了字符串的开头并且进位 的值为 。
最后将答案字符串反转并返回即可。
时间复杂度 ,其中 和 分别是两个字符串的长度。忽略答案字符串的空间消耗,空间复杂度 。
以下代码还实现了字符串相减,参考 subStrings(num1, num2) 函数。
class Solution { public String addStrings(String num1, String num2) { int i = num1.length() - 1, j = num2.length() - 1; StringBuilder ans = new StringBuilder(); for (int c = 0; i >= 0 || j >= 0 || c > 0; --i, --j) { int a = i < 0 ? 0 : num1.charAt(i) - '0'; int b = j < 0 ? 0 : num2.charAt(j) - '0'; c += a + b; ans.append(c % 10); c /= 10; } return ans.reverse().toString(); } public String subStrings(String num1, String num2) { int m = num1.length(), n = num2.length(); boolean neg = m < n || (m == n && num1.compareTo(num2) < 0); if (neg) { String t = num1; num1 = num2; num2 = t; } int i = num1.length() - 1, j = num2.length() - 1; StringBuilder ans = new StringBuilder(); for (int c = 0; i >= 0; --i, --j) { c = (num1.charAt(i) - '0') - c - (j < 0 ? 0 : num2.charAt(j) - '0'); ans.append((c + 10) % 10); c = c < 0 ? 1 : 0; } while (ans.length() > 1 && ans.charAt(ans.length() - 1) == '0') { ans.deleteCharAt(ans.length() - 1); } if (neg) { ans.append('-'); } return ans.reverse().toString(); } }
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100方法一:动态规划
题目可以转换为 0-1 背包问题。
设整数数组总和为 s,要使得数组分割成两个元素和相等的子数组,需要满足 s 能够被 2 整除。在此前提下,我们可以将问题抽象为: 从数组中选出若干个数,使得选出的元素之和为 s/2。显然这是一个 0-1 背包问题。
定义 dp[i][j] 表示是否可以从前 i 个数中选出若干个数,使得所选元素之和为 j。
动态规划——0-1 背包朴素做法:
动态规划——0-1 背包空间优化:
DFS + 记忆化搜索:
class Solution { public boolean canPartition(int[] nums) { int s = 0; for (int v : nums) { s += v; } if (s % 2 != 0) { return false; } int m = nums.length; int n = s >> 1; boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; for (int i = 1; i <= m; ++i) { for (int j = 0; j <= n; ++j) { dp[i][j] = dp[i - 1][j]; if (!dp[i][j] && nums[i - 1] <= j) { dp[i][j] = dp[i - 1][j - nums[i - 1]]; } } } return dp[m][n]; } }
class Solution { public boolean canPartition(int[] nums) { int s = 0; for (int v : nums) { s += v; } if (s % 2 != 0) { return false; } int n = s >> 1; boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int v : nums) { for (int j = n; j >= v; --j) { dp[j] = dp[j] || dp[j - v]; } } return dp[n]; } }
有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。
岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。
示例 1:

输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]] 输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
示例 2:
输入: heights = [[2,1],[1,2]] 输出: [[0,0],[0,1],[1,0],[1,1]]
提示:
m == heights.lengthn == heights[r].length1 <= m, n <= 2000 <= heights[r][c] <= 105反向寻找,从海洋开始逆流,只要比该陆地高的地方就能逆流。最后是寻找两个海洋的水流都能经过的陆地即为结果。
class Solution { private int[][] heights; private int m; private int n; public List<List<Integer>> pacificAtlantic(int[][] heights) { m = heights.length; n = heights[0].length; this.heights = heights; Deque<int[]> q1 = new LinkedList<>(); Deque<int[]> q2 = new LinkedList<>(); Set<Integer> vis1 = new HashSet<>(); Set<Integer> vis2 = new HashSet<>(); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (i == 0 || j == 0) { vis1.add(i * n + j); q1.offer(new int[] {i, j}); } if (i == m - 1 || j == n - 1) { vis2.add(i * n + j); q2.offer(new int[] {i, j}); } } } bfs(q1, vis1); bfs(q2, vis2); List<List<Integer>> ans = new ArrayList<>(); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { int x = i * n + j; if (vis1.contains(x) && vis2.contains(x)) { ans.add(Arrays.asList(i, j)); } } } return ans; } private void bfs(Deque<int[]> q, Set<Integer> vis) { int[] dirs = {-1, 0, 1, 0, -1}; while (!q.isEmpty()) { for (int k = q.size(); k > 0; --k) { int[] p = q.poll(); for (int i = 0; i < 4; ++i) { int x = p[0] + dirs[i]; int y = p[1] + dirs[i + 1]; if (x >= 0 && x < m && y >= 0 && y < n && !vis.contains(x * n + y) && heights[x][y] >= heights[p[0]][p[1]]) { vis.add(x * n + y); q.offer(new int[] {x, y}); } } } } } }
给你一个 rows x cols 的屏幕和一个用 非空 的单词列表组成的句子,请你计算出给定句子可以在屏幕上完整显示的次数。
注意:
rows, cols ≤ 20,000.示例 1:
输入: rows = 2, cols = 8, 句子 sentence = ["hello", "world"] 输出: 1 解释: hello--- world--- 字符 '-' 表示屏幕上的一个空白位置。
示例 2:
输入: rows = 3, cols = 6, 句子 sentence = ["a", "bcd", "e"] 输出: 2 解释: a-bcd- e-a--- bcd-e- 字符 '-' 表示屏幕上的一个空白位置。
示例 3:
输入: rows = 4, cols = 5, 句子 sentence = ["I", "had", "apple", "pie"] 输出: 1 解释: I-had apple pie-I had-- 字符 '-' 表示屏幕上的一个空白位置。
给你一个大小为 m x n 的矩阵 board 表示甲板,其中,每个单元格可以是一艘战舰 'X' 或者是一个空位 '.' ,返回在甲板 board 上放置的 战舰 的数量。
战舰 只能水平或者垂直放置在 board 上。换句话说,战舰只能按 1 x k(1 行,k 列)或 k x 1(k 行,1 列)的形状建造,其中 k 可以是任意大小。两艘战舰之间至少有一个水平或垂直的空位分隔 (即没有相邻的战舰)。
示例 1:
输入:board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]] 输出:2
示例 2:
输入:board = [["."]] 输出:0
提示:
m == board.lengthn == board[i].length1 <= m, n <= 200board[i][j] 是 '.' 或 'X'进阶:你可以实现一次扫描算法,并只使用 O(1) 额外空间,并且不修改 board 的值来解决这个问题吗?
初始化结果数 ans = 0。
遍历二维甲板,若 X 的左方、上方不为 X,则结果 ans 加 1。
class Solution { public int countBattleships(char[][] board) { int m = board.length, n = board[0].length; int ans = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == '.') { continue; } if (i > 0 && board[i - 1][j] == 'X') { continue; } if (j > 0 && board[i][j - 1] == 'X') { continue; } ++ans; } } return ans; } }
满足以下条件的密码被认为是强密码:
6 个,至多 20 个字符组成。"Baaabb0" 是弱密码, 但是 "Baaba0" 是强密码)。给你一个字符串 password ,返回 将 password 修改到满足强密码条件需要的最少修改步数。如果 password 已经是强密码,则返回 0 。
在一步修改操作中,你可以:
password ,password 中删除一个字符,或password 中的某个字符。示例 1:
输入:password = "a" 输出:5
示例 2:
输入:password = "aA1" 输出:3
示例 3:
输入:password = "1337C0d3" 输出:0
提示:
1 <= password.length <= 50password 由字母、数字、点 '.' 或者感叹号 '!' 组成class Solution { public int strongPasswordChecker(String password) { int types = countTypes(password); int n = password.length(); if (n < 6) { return Math.max(6 - n, 3 - types); } char[] chars = password.toCharArray(); if (n <= 20) { int replace = 0; int cnt = 0; char prev = '~'; for (char curr : chars) { if (curr == prev) { ++cnt; } else { replace += cnt / 3; cnt = 1; prev = curr; } } replace += cnt / 3; return Math.max(replace, 3 - types); } int replace = 0, remove = n - 20; int remove2 = 0; int cnt = 0; char prev = '~'; for (char curr : chars) { if (curr == prev) { ++cnt; } else { if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) { ++remove2; } } replace += cnt / 3; cnt = 1; prev = curr; } } if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) { ++remove2; } } replace += cnt / 3; int use2 = Math.min(Math.min(replace, remove2), remove / 2); replace -= use2; remove -= use2 * 2; int use3 = Math.min(replace, remove / 3); replace -= use3; remove -= use3 * 3; return (n - 20) + Math.max(replace, 3 - types); } private int countTypes(String s) { int a = 0, b = 0, c = 0; for (char ch : s.toCharArray()) { if (Character.isLowerCase(ch)) { a = 1; } else if (Character.isUpperCase(ch)) { b = 1; } else if (Character.isDigit(ch)) { c = 1; } } return a + b + c; } }
给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n 。
示例 1:
输入:nums = [3,10,5,25,2,8] 输出:28 解释:最大运算结果是 5 XOR 25 = 28.
示例 2:
输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70] 输出:127
提示:
1 <= nums.length <= 2 * 1050 <= nums[i] <= 231 - 1方法一:哈希表
方法二:前缀树
题目是求两个元素的异或最大值,可以从最高位开始考虑。
我们把数组中的每个元素 看作一个 位的 串,按二进制从高位到低位的顺序,插入前缀树(最低位为叶子节点)。
搜索 时,尽量走相反的 字符指针的策略,因为异或运算的法则是相同得 ,不同得 ,所以我们尽可能往与 当前位相反的字符方向走,才能得到能和 产生最大值的结果。
class Solution { public int findMaximumXOR(int[] numbers) { int max = 0; int mask = 0; for (int i = 30; i >= 0; i--) { int current = 1 << i; // 期望的二进制前缀 mask = mask ^ current; // 在当前前缀下, 数组内的前缀位数所有情况集合 Set<Integer> set = new HashSet<>(); for (int j = 0, k = numbers.length; j < k; j++) { set.add(mask & numbers[j]); } // 期望最终异或值的从右数第i位为1, 再根据异或运算的特性推算假设是否成立 int flag = max | current; for (Integer prefix : set) { if (set.contains(prefix ^ flag)) { max = flag; break; } } } return max; } }
前缀树:
class Trie { Trie[] children = new Trie[2]; void insert(int x) { Trie node = this; for (int i = 30; i >= 0; --i) { int v = (x >> i) & 1; if (node.children[v] == null) { node.children[v] = new Trie(); } node = node.children[v]; } } int search(int x) { Trie node = this; int res = 0; for (int i = 30; i >= 0; --i) { int v = (x >> i) & 1; if (node.children[v ^ 1] != null) { res = res << 1 | 1; node = node.children[v ^ 1]; } else { res <<= 1; node = node.children[v]; } } return res; } } class Solution { public int findMaximumXOR(int[] nums) { Trie trie = new Trie(); int ans = 0; for (int v : nums) { trie.insert(v); ans = Math.max(ans, trie.search(v)); } return ans; } }
给你一个单词序列,判断其是否形成了一个有效的单词方块。
有效的单词方块是指此由单词序列组成的文字方块的 第 k 行 和 第 k 列 (0 ≤ k < max(行数, 列数)) 所显示的字符串完全相同。
注意:
a-z。示例 1:
输入: [ "abcd", "bnrt", "crmy", "dtye" ] 输出: true 解释: 第 1 行和第 1 列都是 "abcd"。 第 2 行和第 2 列都是 "bnrt"。 第 3 行和第 3 列都是 "crmy"。 第 4 行和第 4 列都是 "dtye"。 因此,这是一个有效的单词方块。
示例 2:
输入: [ "abcd", "bnrt", "crm", "dt" ] 输出: true 解释: 第 1 行和第 1 列都是 "abcd"。 第 2 行和第 2 列都是 "bnrt"。 第 3 行和第 3 列都是 "crm"。 第 4 行和第 4 列都是 "dt"。 因此,这是一个有效的单词方块。
示例 3:
输入: [ "ball", "area", "read", "lady" ] 输出: false 解释: 第 3 行是 "read" ,然而第 3 列是 "lead"。 因此,这 不是 一个有效的单词方块。
给你一个字符串 s ,其中包含字母顺序打乱的用英文单词表示的若干数字(0-9)。按 升序 返回原始的数字。
示例 1:
输入:s = "owoztneoer" 输出:"012"
示例 2:
输入:s = "fviefuro" 输出:"45"
提示:
1 <= s.length <= 105s[i] 为 ["e","g","f","i","h","o","n","s","r","u","t","w","v","x","z"] 这些字符之一s 保证是一个符合题目要求的字符串统计 ["e","g","f","i","h","o","n","s","r","u","t","w","v","x","z"] 每个字母在哪些数字出现过。
| 字母 | 数字 |
|---|---|
| e | 0 1 3 5 7 8 9 |
| g | 8 |
| f | 4 5 |
| i | 5 6 8 9 |
| h | 3 8 |
| o | 0 1 2 4 |
| n | 1 7 9 |
| s | 6 7 |
| r | 0 3 4 |
| u | 4 |
| t | 2 3 8 |
| w | 2 |
| v | 5 7 |
| x | 6 |
| z | 0 |
由于部分字母只在某个数字出现过,比如字母 z 只在 0 出现过,因此我们统计英文中 z 的数量,就可以推断数字 0 的个数,依次类推。
class Solution { public String originalDigits(String s) { int[] counter = new int[26]; for (char c : s.toCharArray()) { ++counter[c - 'a']; } int[] cnt = new int[10]; cnt[0] = counter['z' - 'a']; cnt[2] = counter['w' - 'a']; cnt[4] = counter['u' - 'a']; cnt[6] = counter['x' - 'a']; cnt[8] = counter['g' - 'a']; cnt[3] = counter['h' - 'a'] - cnt[8]; cnt[5] = counter['f' - 'a'] - cnt[4]; cnt[7] = counter['s' - 'a'] - cnt[6]; cnt[1] = counter['o' - 'a'] - cnt[0] - cnt[2] - cnt[4]; cnt[9] = counter['i' - 'a'] - cnt[5] - cnt[6] - cnt[8]; StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10; ++i) { for (int j = 0; j < cnt[i]; ++j) { sb.append(i); } } return sb.toString(); } }
给你一个字符串 s 和一个整数 k 。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k 次。
在执行上述操作后,返回包含相同字母的最长子字符串的长度。
示例 1:
输入:s = "ABAB", k = 2 输出:4 解释:用两个'A'替换为两个'B',反之亦然。
示例 2:
输入:s = "AABABBA", k = 1 输出:4 解释: 将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。 子串 "BBBB" 有最长重复字母, 答案为 4。 可能存在其他的方法来得到同样的结果。
提示:
1 <= s.length <= 105s 仅由大写英文字母组成0 <= k <= s.length我们维护一个数组 int[26] 来存储当前窗口中各个字母的出现次数,j 表示窗口的左边界,i 表示窗口右边界。
maxCnt 保存滑动窗口内相同字母出现次数的历史最大值,通过判断窗口宽度 i - j + 1 是否大于 maxCnt + k 来决定窗口是否做滑动,否则窗口就扩张。
class Solution { public int characterReplacement(String s, int k) { int[] counter = new int[26]; int i = 0; int j = 0; for (int maxCnt = 0; i < s.length(); ++i) { char c = s.charAt(i); ++counter[c - 'A']; maxCnt = Math.max(maxCnt, counter[c - 'A']); if (i - j + 1 - maxCnt > k) { --counter[s.charAt(j) - 'A']; ++j; } } return i - j; } }
给定一个单词集合 words (没有重复),找出并返回其中所有的 单词方块 。 words 中的同一个单词可以被 多次 使用。你可以按 任意顺序 返回答案。
一个单词序列形成了一个有效的 单词方块 的意思是指从第 k 行和第 k 列 0 <= k < max(numRows, numColumns) 来看都是相同的字符串。
["ball","area","lead","lady"] 形成了一个单词方块,因为每个单词从水平方向看和从竖直方向看都是相同的。示例 1:
输入:words = ["area","lead","wall","lady","ball"] 输出: [["ball","area","lead","lady"], ["wall","area","lead","lady"]] 解释: 输出包含两个单词方块,输出的顺序不重要,只需要保证每个单词方块内的单词顺序正确即可。
示例 2:
输入:words = ["abat","baba","atan","atal"] 输出:[["baba","abat","baba","atal"], ["baba","abat","baba","atan"]] 解释: 输出包含两个单词方块,输出的顺序不重要,只需要保证每个单词方块内的单词顺序正确即可。
提示:
1 <= words.length <= 10001 <= words[i].length <= 4words[i] 长度相同words[i] 只由小写英文字母组成words[i] 都 各不相同方法一:前缀树 + DFS
根据已添加单词确定下一个单词的前缀,继续进行搜索。
比如已经添加了两个单词 和 ,要添加下一个单词,我们首先要获取下一个单词的前缀,第一个字母是第一个单词的第三个位置 ,第二个字母是第二个单词的第三个位置 ,这样就构成了前缀 。然后找出所有前缀为 的单词,作为下一个单词。
class Trie { Trie[] children = new Trie[26]; List<Integer> v = new ArrayList<>(); void insert(String word, int i) { Trie node = this; for (char c : word.toCharArray()) { c -= 'a'; if (node.children[c] == null) { node.children[c] = new Trie(); } node = node.children[c]; node.v.add(i); } } List<Integer> search(String pref) { Trie node = this; for (char c : pref.toCharArray()) { c -= 'a'; if (node.children[c] == null) { return Collections.emptyList(); } node = node.children[c]; } return node.v; } } class Solution { private Trie trie = new Trie(); private String[] words; private List<List<String>> ans = new ArrayList<>(); public List<List<String>> wordSquares(String[] words) { this.words = words; for (int i = 0; i < words.length; ++i) { trie.insert(words[i], i); } List<String> t = new ArrayList<>(); for (String w : words) { t.add(w); dfs(t); t.remove(t.size() - 1); } return ans; } private void dfs(List<String> t) { if (t.size() == words[0].length()) { ans.add(new ArrayList<>(t)); return; } int idx = t.size(); StringBuilder pref = new StringBuilder(); for (String x : t) { pref.append(x.charAt(idx)); } List<Integer> indexes = trie.search(pref.toString()); for (int i : indexes) { t.add(words[i]); dfs(t); t.remove(t.size() - 1); } } }
将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。
对于双向循环列表,你可以将左右孩子指针作为双向循环链表的前驱和后继指针,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
特别地,我们希望可以 就地 完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中最小元素的指针。
示例 1:
输入:root = [4,2,5,1,3]输出:[1,2,3,4,5] 解释:下图显示了转化后的二叉搜索树,实线表示后继关系,虚线表示前驱关系。
![]()
示例 2:
输入:root = [2,1,3] 输出:[1,2,3]
示例 3:
输入:root = [] 输出:[] 解释:输入是空树,所以输出也是空链表。
示例 4:
输入:root = [1] 输出:[1]
提示:
-1000 <= Node.val <= 1000Node.left.val < Node.val < Node.right.valNode.val 的所有值都是独一无二的0 <= Number of Nodes <= 2000prev.right = cur、cur.left = prev、prev = cur/* // Definition for a Node. class Node { public int val; public Node left; public Node right; public Node() {} public Node(int _val) { val = _val; } public Node(int _val,Node _left,Node _right) { val = _val; left = _left; right = _right; } }; */ class Solution { private Node prev; private Node head; public Node treeToDoublyList(Node root) { if (root == null) { return null; } prev = null; head = null; dfs(root); prev.right = head; head.left = prev; return head; } private void dfs(Node root) { if (root == null) { return; } dfs(root.left); if (prev != null) { prev.right = root; root.left = prev; } else { head = root; } prev = root; dfs(root.right); } }
给你一个 n * n 矩阵 grid ,矩阵由若干 0 和 1 组成。请你用四叉树表示该矩阵 grid 。
你需要返回能表示矩阵的 四叉树 的根结点。
注意,当 isLeaf 为 False 时,你可以把 True 或者 False 赋值给节点,两种值都会被判题机制 接受 。
四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:
val:储存叶子结点所代表的区域的值。1 对应 True,0 对应 False;isLeaf: 当这个节点是一个叶子结点时为 True,如果它有 4 个子节点则为 False 。class Node {
public boolean val;
public boolean isLeaf;
public Node topLeft;
public Node topRight;
public Node bottomLeft;
public Node bottomRight;
}
我们可以按以下步骤为二维区域构建四叉树:
0 或者全为 1),将 isLeaf 设为 True ,将 val 设为网格相应的值,并将四个子节点都设为 Null 然后停止。isLeaf 设为 False, 将 val 设为任意值,然后如下图所示,将当前网格划分为四个子网格。
如果你想了解更多关于四叉树的内容,可以参考 wiki 。
四叉树格式:
输出为使用层序遍历后四叉树的序列化形式,其中 null 表示路径终止符,其下面不存在节点。
它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 [isLeaf, val] 。
如果 isLeaf 或者 val 的值为 True ,则表示它在列表 [isLeaf, val] 中的值为 1 ;如果 isLeaf 或者 val 的值为 False ,则表示值为 0 。
示例 1:

输入:grid = [[0,1],[1,0]] 输出:[[0,1],[1,0],[1,1],[1,1],[1,0]] 解释:此示例的解释如下: 请注意,在下面四叉树的图示中,0 表示 false,1 表示 True 。![]()
示例 2:

输入:grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]] 输出:[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]] 解释:网格中的所有值都不相同。我们将网格划分为四个子网格。 topLeft,bottomLeft 和 bottomRight 均具有相同的值。 topRight 具有不同的值,因此我们将其再分为 4 个子网格,这样每个子网格都具有相同的值。 解释如下图所示:![]()
示例 3:
输入:grid = [[1,1],[1,1]] 输出:[[1,1]]
示例 4:
输入:grid = [[0]] 输出:[[1,0]]
示例 5:
输入:grid = [[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]] 输出:[[0,1],[1,1],[1,0],[1,0],[1,1]]
提示:
n == grid.length == grid[i].lengthn == 2^x 其中 0 <= x <= 6方法一:DFS
DFS 递归遍历 grid,先判断 grid 是否为叶子节点,是则返回叶子节点相关信息;否则递归 grid 4 个子节点。
/* // Definition for a QuadTree node. class Node { public boolean val; public boolean isLeaf; public Node topLeft; public Node topRight; public Node bottomLeft; public Node bottomRight; public Node() { this.val = false; this.isLeaf = false; this.topLeft = null; this.topRight = null; this.bottomLeft = null; this.bottomRight = null; } public Node(boolean val, boolean isLeaf) { this.val = val; this.isLeaf = isLeaf; this.topLeft = null; this.topRight = null; this.bottomLeft = null; this.bottomRight = null; } public Node(boolean val, boolean isLeaf, Node topLeft, Node topRight, Node bottomLeft, Node bottomRight) { this.val = val; this.isLeaf = isLeaf; this.topLeft = topLeft; this.topRight = topRight; this.bottomLeft = bottomLeft; this.bottomRight = bottomRight; } }; */ class Solution { public Node construct(int[][] grid) { return dfs(0, 0, grid.length - 1, grid[0].length - 1, grid); } private Node dfs(int a, int b, int c, int d, int[][] grid) { int zero = 0, one = 0; for (int i = a; i <= c; ++i) { for (int j = b; j <= d; ++j) { if (grid[i][j] == 0) { zero = 1; } else { one = 1; } } } boolean isLeaf = zero + one == 1; boolean val = isLeaf && one == 1; Node node = new Node(val, isLeaf); if (isLeaf) { return node; } node.topLeft = dfs(a, b, (a + c) / 2, (b + d) / 2, grid); node.topRight = dfs(a, (b + d) / 2 + 1, (a + c) / 2, d, grid); node.bottomLeft = dfs((a + c) / 2 + 1, b, c, (b + d) / 2, grid); node.bottomRight = dfs((a + c) / 2 + 1, (b + d) / 2 + 1, c, d, grid); return node; } }
序列化是指将一个数据结构转化为位序列的过程,因此可以将其存储在文件中或内存缓冲区中,以便稍后在相同或不同的计算机环境中恢复结构。
设计一个序列化和反序列化 N 叉树的算法。一个 N 叉树是指每个节点都有不超过 N 个孩子节点的有根树。序列化 / 反序列化算法的算法实现没有限制。你只需要保证 N 叉树可以被序列化为一个字符串并且该字符串可以被反序列化成原树结构即可。
例如,你需要序列化下面的 3-叉 树。

为 [1 [3[5 6] 2 4]]。你不需要以这种形式完成,你可以自己创造和实现不同的方法。
或者,您可以遵循 LeetCode 的层序遍历序列化格式,其中每组孩子节点由空值分隔。

例如,上面的树可以序列化为 [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
你不一定要遵循以上建议的格式,有很多不同的格式,所以请发挥创造力,想出不同的方法来完成本题。
示例 1:
输入: root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14] 输出: [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
示例 2:
输入: root = [1,null,3,2,4,null,5,6] 输出: [1,null,3,2,4,null,5,6]
示例 3:
输入: root = [] 输出: []
提示:
[0, 104].0 <= Node.val <= 1041000给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
示例 1:

输入:root = [1,null,3,2,4,null,5,6] 输出:[[1],[3,2,4],[5,6]]
示例 2:

输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14] 输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
提示:
1000[0, 10^4] 之间方法一:BFS
借助队列,逐层遍历。
时间复杂度 。
方法二:DFS
按深度遍历。
假设当前深度为 i,遍历到的节点为 root。若结果列表 ans[i] 不存在,则创建一个空列表放入 ans 中,然后将 root.val 放入 ans[i]。接着往下一层遍历(root 的子节点)。
时间复杂度 。
BFS:
DFS:
BFS:
/* // Definition for a Node. class Node { public int val; public List<Node> children; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, List<Node> _children) { val = _val; children = _children; } }; */ class Solution { public List<List<Integer>> levelOrder(Node root) { List<List<Integer>> ans = new ArrayList<>(); if (root == null) { return ans; } Deque<Node> q = new ArrayDeque<>(); q.offer(root); while (!q.isEmpty()) { List<Integer> t = new ArrayList<>(); for (int n = q.size(); n > 0; --n) { root = q.poll(); t.add(root.val); q.addAll(root.children); } ans.add(t); } return ans; } }
DFS:
/* // Definition for a Node. class Node { public int val; public List<Node> children; public Node() {} public Node(int _val) { val = _val; } public Node(int _val, List<Node> _children) { val = _val; children = _children; } }; */ class Solution { public List<List<Integer>> levelOrder(Node root) { List<List<Integer>> ans = new ArrayList<>(); dfs(root, 0, ans); return ans; } private void dfs(Node root, int i, List<List<Integer>> ans) { if (root == null) { return; } if (ans.size() <= i) { ans.add(new ArrayList<>()); } ans.get(i++).add(root.val); for (Node child : root.children) { dfs(child, i, ans); } } }
BFS:
DFS:
BFS:
DFS:
BFS:
DFS:
你会得到一个双链表,其中包含的节点有一个下一个指针、一个前一个指针和一个额外的 子指针 。这个子指针可能指向一个单独的双向链表,也包含这些特殊的节点。这些子列表可以有一个或多个自己的子列表,以此类推,以生成如下面的示例所示的 多层数据结构 。
给定链表的头节点 head ,将链表 扁平化 ,以便所有节点都出现在单层双链表中。让 curr 是一个带有子列表的节点。子列表中的节点应该出现在扁平化列表中的 curr 之后 和 curr.next 之前 。
返回 扁平列表的 head 。列表中的节点必须将其 所有 子指针设置为 null 。
示例 1:

输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12] 输出:[1,2,3,7,8,11,12,9,10,4,5,6] 解释:输入的多级列表如上图所示。 扁平化后的链表如下图:![]()
示例 2:
输入:head = [1,2,null,3] 输出:[1,3,2] 解释:输入的多级列表如上图所示。 扁平化后的链表如下图:![]()
示例 3:
输入:head = [] 输出:[] 说明:输入中可能存在空列表。
提示:
10001 <= Node.val <= 105如何表示测试用例中的多级链表?
以 示例 1 为例:
1---2---3---4---5---6--NULL
|
7---8---9---10--NULL
|
11--12--NULL
序列化其中的每一级之后:
[1,2,3,4,5,6,null] [7,8,9,10,null] [11,12,null]
为了将每一级都序列化到一起,我们需要每一级中添加值为 null 的元素,以表示没有节点连接到上一级的上级节点。
[1,2,3,4,5,6,null] [null,null,7,8,9,10,null] [null,11,12,null]
合并所有序列化结果,并去除末尾的 null 。
[1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
/* // Definition for a Node. class Node { public int val; public Node prev; public Node next; public Node child; }; */ class Solution { public Node flatten(Node head) { if (head == null) { return null; } Node dummy = new Node(); dummy.next = head; preorder(dummy, head); dummy.next.prev = null; return dummy.next; } private Node preorder(Node pre, Node cur) { if (cur == null) { return pre; } cur.prev = pre; pre.next = cur; Node t = cur.next; Node tail = preorder(cur, cur.child); cur.child = null; return preorder(tail, t); } }
设计一个算法,可以将 N 叉树编码为二叉树,并能将该二叉树解码为原 N 叉树。一个 N 叉树是指每个节点都有不超过 N 个孩子节点的有根树。类似地,一个二叉树是指每个节点都有不超过 2 个孩子节点的有根树。你的编码 / 解码的算法的实现没有限制,你只需要保证一个 N 叉树可以编码为二叉树且该二叉树可以解码回原始 N 叉树即可。
例如,你可以将下面的 3-叉 树以该种方式编码:

注意,上面的方法仅仅是一个例子,可能可行也可能不可行。你没有必要遵循这种形式转化,你可以自己创造和实现不同的方法。
注意:
N 的范围在 [1, 1000]请你设计一个用于存储字符串计数的数据结构,并能够返回计数最小和最大的字符串。
实现 AllOne 类:
AllOne() 初始化数据结构的对象。inc(String key) 字符串 key 的计数增加 1 。如果数据结构中尚不存在 key ,那么插入计数为 1 的 key 。dec(String key) 字符串 key 的计数减少 1 。如果 key 的计数在减少后为 0 ,那么需要将这个 key 从数据结构中删除。测试用例保证:在减少计数前,key 存在于数据结构中。getMaxKey() 返回任意一个计数最大的字符串。如果没有元素存在,返回一个空字符串 "" 。getMinKey() 返回任意一个计数最小的字符串。如果没有元素存在,返回一个空字符串 "" 。注意:每个函数都应当满足 O(1) 平均时间复杂度。
示例:
输入
["AllOne", "inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"]
[[], ["hello"], ["hello"], [], [], ["leet"], [], []]
输出
[null, null, null, "hello", "hello", null, "hello", "leet"]
解释
AllOne allOne = new AllOne();
allOne.inc("hello");
allOne.inc("hello");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "hello"
allOne.inc("leet");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "leet"
提示:
1 <= key.length <= 10key 由小写英文字母组成dec 时,数据结构中总存在 keyinc、dec、getMaxKey 和 getMinKey 方法 5 * 104 次class AllOne { Node root = new Node(); Map<String, Node> nodes = new HashMap<>(); public AllOne() { root.next = root; root.prev = root; } public void inc(String key) { if (!nodes.containsKey(key)) { if (root.next == root || root.next.cnt > 1) { nodes.put(key, root.insert(new Node(key, 1))); } else { root.next.keys.add(key); nodes.put(key, root.next); } } else { Node curr = nodes.get(key); Node next = curr.next; if (next == root || next.cnt > curr.cnt + 1) { nodes.put(key, curr.insert(new Node(key, curr.cnt + 1))); } else { next.keys.add(key); nodes.put(key, next); } curr.keys.remove(key); if (curr.keys.isEmpty()) { curr.remove(); } } } public void dec(String key) { Node curr = nodes.get(key); if (curr.cnt == 1) { nodes.remove(key); } else { Node prev = curr.prev; if (prev == root || prev.cnt < curr.cnt - 1) { nodes.put(key, prev.insert(new Node(key, curr.cnt - 1))); } else { prev.keys.add(key); nodes.put(key, prev); } } curr.keys.remove(key); if (curr.keys.isEmpty()) { curr.remove(); } } public String getMaxKey() { return root.prev.keys.iterator().next(); } public String getMinKey() { return root.next.keys.iterator().next(); } } class Node { Node prev; Node next; int cnt; Set<String> keys = new HashSet<>(); public Node() { this("", 0); } public Node(String key, int cnt) { this.cnt = cnt; keys.add(key); } public Node insert(Node node) { node.prev = this; node.next = this.next; node.prev.next = node; node.next.prev = node; return node; } public void remove() { this.prev.next = this.next; this.next.prev = this.prev; } } /** * Your AllOne object will be instantiated and called as such: * AllOne obj = new AllOne(); * obj.inc(key); * obj.dec(key); * String param_3 = obj.getMaxKey(); * String param_4 = obj.getMinKey(); */
基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。
假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。
"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中)
给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。
注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。
示例 1:
输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"] 输出:1
示例 2:
输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"] 输出:2
示例 3:
输入:start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"] 输出:3
提示:
start.length == 8end.length == 80 <= bank.length <= 10bank[i].length == 8start、end 和 bank[i] 仅由字符 ['A', 'C', 'G', 'T'] 组成方法一:BFS
方法二:DFS
class Solution { public int minMutation(String start, String end, String[] bank) { Set<String> s = new HashSet<>(); for (String b : bank) { s.add(b); } Map<Character, String> mp = new HashMap<>(4); mp.put('A', "TCG"); mp.put('T', "ACG"); mp.put('C', "ATG"); mp.put('G', "ATC"); Deque<Pair<String, Integer>> q = new LinkedList<>(); q.offer(new Pair<>(start, 0)); while (!q.isEmpty()) { Pair<String, Integer> p = q.poll(); String t = p.getKey(); int step = p.getValue(); if (end.equals(t)) { return step; } for (int i = 0; i < t.length(); ++i) { for (char c : mp.get(t.charAt(i)).toCharArray()) { String next = t.substring(0, i) + c + t.substring(i + 1); if (s.contains(next)) { q.offer(new Pair<>(next, step + 1)); s.remove(next); } } } } return -1; } }
class Solution { private int ans; private Set<String> s; private static final char[] seq = {'A', 'C', 'G', 'T'}; public int minMutation(String start, String end, String[] bank) { s = new HashSet<>(); for (String b : bank) { s.add(b); } ans = Integer.MAX_VALUE; dfs(start, end, 0); s.remove(start); return ans == Integer.MAX_VALUE ? -1 : ans; } private void dfs(String start, String end, int t) { if (start.equals(end)) { ans = Math.min(ans, t); return; } for (int i = 0; i < start.length(); ++i) { for (char c : seq) { if (start.charAt(i) == c) { continue; } String nxt = start.substring(0, i) + c + start.substring(i + 1); if (s.contains(nxt)) { s.remove(nxt); dfs(nxt, end, t + 1); } } } } }
统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。
请注意,你可以假定字符串里不包括任何不可打印的字符。
示例:
输入: "Hello, my name is John" 输出: 5 解释: 这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。
方法一:字符串分割
将字符串 s 按照空格进行分割,然后统计不为空的单词个数。
时间复杂度 ,空间复杂度 。
方法二:模拟
直接模拟,遍历字符串,检测每个字符,统计个数。
时间复杂度 ,空间复杂度 。
class Solution { public int countSegments(String s) { int ans = 0; for (String t : s.split(" ")) { if (!"".equals(t)) { ++ans; } } return ans; } }
class Solution { public int countSegments(String s) { int ans = 0; for (int i = 0; i < s.length(); ++i) { if (s.charAt(i) != ' ' && (i == 0 || s.charAt(i - 1) == ' ')) { ++ans; } } return ans; } }
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]] 输出: 1 解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ] 输出: 0 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
提示:
1 <= intervals.length <= 105intervals[i].length == 2-5 * 104 <= starti < endi <= 5 * 104方法一:转换为最长上升子序列问题
最长上升子序列问题,动态规划的做法,时间复杂度是 ,这里可以采用贪心优化,将复杂度降至 。
方法二:排序 + 贪心
先按照区间右边界排序。优先选择最小的区间的右边界作为起始边界。遍历区间:
最后返回 ans 即可。
时间复杂度 。
class Solution { public int eraseOverlapIntervals(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a -> a[1])); int t = intervals[0][1], ans = 0; for (int i = 1; i < intervals.length; ++i) { if (intervals[i][0] >= t) { t = intervals[i][1]; } else { ++ans; } } return ans; } }
class Solution { public int eraseOverlapIntervals(int[][] intervals) { Arrays.sort(intervals, (a, b) -> { if (a[0] != b[0]) { return a[0] - b[0]; } return a[1] - b[1]; }); int n = intervals.length; int[] d = new int[n + 1]; d[1] = intervals[0][1]; int size = 1; for (int i = 1; i < n; ++i) { int s = intervals[i][0], e = intervals[i][1]; if (s >= d[size]) { d[++size] = e; } else { int left = 1, right = size; while (left < right) { int mid = (left + right) >> 1; if (d[mid] >= s) { right = mid; } else { left = mid + 1; } } d[left] = Math.min(d[left], e); } } return n - size; } }
给你一个区间数组 intervals ,其中 intervals[i] = [starti, endi] ,且每个 starti 都 不同 。
区间 i 的 右侧区间 可以记作区间 j ,并满足 startj >= endi ,且 startj 最小化 。注意 i 可能等于 j 。
返回一个由每个区间 i 的 右侧区间 在 intervals 中对应下标组成的数组。如果某个区间 i 不存在对应的 右侧区间 ,则下标 i 处的值设为 -1 。
示例 1:
输入:intervals = [[1,2]] 输出:[-1] 解释:集合中只有一个区间,所以输出-1。
示例 2:
输入:intervals = [[3,4],[2,3],[1,2]] 输出:[-1,0,1] 解释:对于 [3,4] ,没有满足条件的“右侧”区间。 对于 [2,3] ,区间[3,4]具有最小的“右”起点; 对于 [1,2] ,区间[2,3]具有最小的“右”起点。
示例 3:
输入:intervals = [[1,4],[2,3],[3,4]] 输出:[-1,2,-1] 解释:对于区间 [1,4] 和 [3,4] ,没有满足条件的“右侧”区间。 对于 [2,3] ,区间 [3,4] 有最小的“右”起点。
提示:
1 <= intervals.length <= 2 * 104intervals[i].length == 2-106 <= starti <= endi <= 106方法一:二分查找
class Solution { public int[] findRightInterval(int[][] intervals) { int n = intervals.length; List<int[]> starts = new ArrayList<>(); for (int i = 0; i < n; ++i) { starts.add(new int[] {intervals[i][0], i}); } starts.sort(Comparator.comparingInt(a -> a[0])); int[] res = new int[n]; int i = 0; for (int[] interval : intervals) { int left = 0, right = n - 1; int end = interval[1]; while (left < right) { int mid = (left + right) >> 1; if (starts.get(mid)[0] >= end) { right = mid; } else { left = mid + 1; } } res[i++] = starts.get(left)[0] < end ? -1 : starts.get(left)[1]; } return res; } }
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8 输出:3 解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 输出:3
提示:
[0,1000]-109 <= Node.val <= 109 -1000 <= targetSum <= 1000 方法一:哈希表 + 前缀和 + 递归
我们可以运用前缀和的思想,对二叉树进行递归遍历,同时用哈希表 统计从根节点到当前节点的路径上各个前缀和出现的次数。
我们设计一个递归函数 ,表示当前遍历到的节点为 ,从根节点到当前节点的路径上的前缀和为 。函数的返回值是统计以 节点及其子树节点作为路径终点且路径和为 的路径数目。那么答案就是 。
函数 的递归过程如下:
时间复杂度 ,空间复杂度 。其中 是二叉树的节点个数。
class Solution { private Map<Long, Integer> cnt = new HashMap<>(); private int targetSum; public int pathSum(TreeNode root, int targetSum) { cnt.put(0L, 1); this.targetSum = targetSum; return dfs(root, 0); } private int dfs(TreeNode node, long s) { if (node == null) { return 0; } s += node.val; int ans = cnt.getOrDefault(s - targetSum, 0); cnt.merge(s, 1, Integer::sum); ans += dfs(node.left, s); ans += dfs(node.right, s); cnt.merge(s, -1, Integer::sum); return ans; } }
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104s 和 p 仅包含小写字母“双指针 + 滑动窗口”求解。定义滑动窗口的左右两个指针 left、right,right 一步步往右走遍历 s 字符串,当 right 指针遍历到的字符加入 t 后超过 counter 的字符数量时,将滑动窗口左侧字符不断弹出,也就是 left 指针不断右移,直到符合要求为止。
若滑动窗口长度等于字符串 p 的长度时,这时的 s 子字符串就是 p 的异位词。
“暴力”求解。利用哈希表 counter 统计字符串 p 中每个字符出现的次数。然后遍历字符串 s,判断子串 s[i, i + p) 中每个字符出现的次数是否与 counter 相同。若是,则将当前下标 i 添加到结果列表中。时间复杂度 O(s.length * p.length)。
class Solution { public List<Integer> findAnagrams(String s, String p) { int[] counter = new int[26]; for (char c : p.toCharArray()) { ++counter[c - 'a']; } List<Integer> ans = new ArrayList<>(); for (int i = 0; i + p.length() - 1 < s.length(); ++i) { int[] t = Arrays.copyOf(counter, counter.length); boolean find = true; for (int j = i; j < i + p.length(); ++j) { if (--t[s.charAt(j) - 'a'] < 0) { find = false; break; } } if (find) { ans.add(i); } } return ans; } }
“双指针 + 滑动窗口”求解。
class Solution { public List<Integer> findAnagrams(String s, String p) { int[] counter = new int[26]; for (char c : p.toCharArray()) { ++counter[c - 'a']; } List<Integer> ans = new ArrayList<>(); int left = 0, right = 0; int[] t = new int[26]; while (right < s.length()) { int i = s.charAt(right) - 'a'; ++t[i]; while (t[i] > counter[i]) { --t[s.charAt(left) - 'a']; ++left; } if (right - left + 1 == p.length()) { ans.add(left); } ++right; } return ans; } }
给定一个表示任意嵌套三元表达式的字符串 expression ,求值并返回其结果。
你可以总是假设给定的表达式是有效的,并且只包含数字, '?' , ':' , 'T' 和 'F' ,其中 'T' 为真, 'F' 为假。表达式中的所有数字都是 一位 数(即在 [0,9] 范围内)。
条件表达式从右到左分组(大多数语言中都是这样),表达式的结果总是为数字 'T' 或 'F' 。
示例 1:
输入: expression = "T?2:3" 输出: "2" 解释: 如果条件为真,结果为 2;否则,结果为 3。
示例 2:
输入: expression = "F?1:T?4:5" 输出: "4" 解释: 条件表达式自右向左结合。使用括号的话,相当于: "(F ? 1 : (T ? 4 : 5))" --> "(F ? 1 : 4)" --> "4" or "(F ? 1 : (T ? 4 : 5))" --> "(T ? 4 : 5)" --> "4"
示例 3:
输入: expression = "T?T?F:5:3" 输出: "F" 解释: 条件表达式自右向左结合。使用括号的话,相当于: "(T ? (T ? F : 5) : 3)" --> "(T ? F : 3)" --> "F" "(T ? (T ? F : 5) : 3)" --> "(T ? F : 5)" --> "F"
提示:
5 <= expression.length <= 104expression 由数字, 'T', 'F', '?' 和 ':' 组成方法一:栈
我们从右到左遍历字符串 expression,对于当前遍历到的字符 :
':',则跳过;'?',那么意味着下一个即将遍历到的字符是条件表达式的条件,我们用一个布尔变量 cond 标记;'?',也即布尔变量 cond 为 true,那么我们判断当前字符 是否为字符 'T',如果是,那么我们要保留栈顶第一个元素,弹出栈顶第二个元素;否则,我们要保留栈顶第二个元素,弹出栈顶第一个元素。最后,将 cond 置为 false;最后,栈中只剩下一个元素,即为表达式的结果。
时间复杂度 ,空间复杂度 。其中 为字符串 expression 的长度。
class Solution { public String parseTernary(String expression) { Deque<Character> stk = new ArrayDeque<>(); boolean cond = false; for (int i = expression.length() - 1; i >= 0; --i) { char c = expression.charAt(i); if (c == ':') { continue; } if (c == '?') { cond = true; } else { if (cond) { if (c == 'T') { char x = stk.pop(); stk.pop(); stk.push(x); } else { stk.pop(); } cond = false; } else { stk.push(c); } } } return String.valueOf(stk.peek()); } }
给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。
示例 1:
输入: n = 13, k = 2 输出: 10 解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。
示例 2:
输入: n = 1, k = 1 输出: 1
提示:
1 <= k <= n <= 109class Solution { private int n; public int findKthNumber(int n, int k) { this.n = n; long curr = 1; --k; while (k > 0) { int cnt = count(curr); if (k >= cnt) { k -= cnt; ++curr; } else { --k; curr *= 10; } } return (int) curr; } public int count(long curr) { long next = curr + 1; long cnt = 0; while (curr <= n) { cnt += Math.min(n - curr + 1, next - curr); next *= 10; curr *= 10; } return (int) cnt; } }
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。
给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。
示例 1:
输入:n = 5 输出:2 解释:因为第三行不完整,所以返回 2 。
示例 2:
输入:n = 8 输出:3 解释:因为第四行不完整,所以返回 3 。
提示:
1 <= n <= 231 - 1方法一:数学推导
(1 + x) * x / 2 <= n,求解 x。
(x + 1/2)² <= 2n + 1/4,即 x <= sqrt(2n + 1/4) - 1/2。
由于 2n 可能溢出,故转换为 x <= sqrt(2) * sqrt(n + 1/8) - 1/2。
方法二:二分查找
class Solution { public int arrangeCoins(int n) { return (int) (Math.sqrt(2) * Math.sqrt(n + 0.125) - 0.5); } }
class Solution { public int arrangeCoins(int n) { long left = 1, right = n; while (left < right) { long mid = (left + right + 1) >>> 1; if ((1 + mid) * mid / 2 <= n) { left = mid; } else { right = mid - 1; } } return (int) left; } }
给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数,并以数组形式返回。
你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间的算法解决此问题。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1] 输出:[2,3]
示例 2:
输入:nums = [1,1,2] 输出:[1]
示例 3:
输入:nums = [1] 输出:[]
提示:
n == nums.length1 <= n <= 1051 <= nums[i] <= nnums 中的每个元素出现 一次 或 两次class Solution { public List<Integer> findDuplicates(int[] nums) { int n = nums.length; for (int i = 0; i < n; ++i) { while (nums[i] != nums[nums[i] - 1]) { swap(nums, i, nums[i] - 1); } } List<Integer> ans = new ArrayList<>(); for (int i = 0; i < n; ++i) { if (nums[i] != i + 1) { ans.add(nums[i]); } } return ans; } void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } }
给你一个字符数组 chars ,请使用下述算法压缩:
从一个空字符串 s 开始。对于 chars 中的每组 连续重复字符 :
1 ,则将字符追加到 s 中。s 追加字符,后跟这一组的长度。压缩后得到的字符串 s 不应该直接返回 ,需要转储到字符数组 chars 中。需要注意的是,如果组长度为 10 或 10 以上,则在 chars 数组中会被拆分为多个字符。
请在 修改完输入数组后 ,返回该数组的新长度。
你必须设计并实现一个只使用常量额外空间的算法来解决此问题。
示例 1:
输入:chars = ["a","a","b","b","c","c","c"] 输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"] 解释:"aa" 被 "a2" 替代。"bb" 被 "b2" 替代。"ccc" 被 "c3" 替代。
示例 2:
输入:chars = ["a"] 输出:返回 1 ,输入数组的前 1 个字符应该是:["a"] 解释:唯一的组是“a”,它保持未压缩,因为它是一个字符。
示例 3:
输入:chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"] 输出:返回 4 ,输入数组的前 4 个字符应该是:["a","b","1","2"]。 解释:由于字符 "a" 不重复,所以不会被压缩。"bbbbbbbbbbbb" 被 “b12” 替代。
提示:
1 <= chars.length <= 2000chars[i] 可以是小写英文字母、大写英文字母、数字或符号双指针。
class Solution { public int compress(char[] chars) { int k = 0, n = chars.length; for (int i = 0, j = i + 1; i < n;) { while (j < n && chars[j] == chars[i]) { ++j; } chars[k++] = chars[i]; if (j - i > 1) { String cnt = String.valueOf(j - i); for (char c : cnt.toCharArray()) { chars[k++] = c; } } i = j; } return k; } }
给定一个长度为 n 的整数数组 nums ,其中 nums 是范围为 [1,n] 的整数的排列。还提供了一个 2D 整数数组 sequences ,其中 sequences[i] 是 nums 的子序列。
检查 nums 是否是唯一的最短 超序列 。最短 超序列 是 长度最短 的序列,并且所有序列 sequences[i] 都是它的子序列。对于给定的数组 sequences ,可能存在多个有效的 超序列 。
sequences = [[1,2],[1,3]] ,有两个最短的 超序列 ,[1,2,3] 和 [1,3,2] 。sequences = [[1,2],[1,3],[1,2,3]] ,唯一可能的最短 超序列 是 [1,2,3] 。[1,2,3,4] 是可能的超序列,但不是最短的。如果 nums 是序列的唯一最短 超序列 ,则返回 true ,否则返回 false 。
子序列 是一个可以通过从另一个序列中删除一些元素或不删除任何元素,而不改变其余元素的顺序的序列。
示例 1:
输入:nums = [1,2,3], sequences = [[1,2],[1,3]] 输出:false 解释:有两种可能的超序列:[1,2,3]和[1,3,2]。 序列 [1,2] 是[1,2,3]和[1,3,2]的子序列。 序列 [1,3] 是[1,2,3]和[1,3,2]的子序列。 因为 nums 不是唯一最短的超序列,所以返回false。
示例 2:
输入:nums = [1,2,3], sequences = [[1,2]] 输出:false 解释:最短可能的超序列为 [1,2]。 序列 [1,2] 是它的子序列:[1,2]。 因为 nums 不是最短的超序列,所以返回false。
示例 3:
输入:nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]] 输出:true 解释:最短可能的超序列为[1,2,3]。 序列 [1,2] 是它的一个子序列:[1,2,3]。 序列 [1,3] 是它的一个子序列:[1,2,3]。 序列 [2,3] 是它的一个子序列:[1,2,3]。 因为 nums 是唯一最短的超序列,所以返回true。
提示:
n == nums.length1 <= n <= 104nums 是 [1, n] 范围内所有整数的排列1 <= sequences.length <= 1041 <= sequences[i].length <= 1041 <= sum(sequences[i].length) <= 1051 <= sequences[i][j] <= nsequences 的所有数组都是 唯一 的sequences[i] 是 nums 的一个子序列方法一:拓扑排序
BFS 实现。
class Solution { public boolean sequenceReconstruction(int[] nums, List<List<Integer>> sequences) { int n = nums.length; int[] indeg = new int[n]; List<Integer>[] g = new List[n]; Arrays.setAll(g, k -> new ArrayList<>()); for (var seq : sequences) { for (int i = 1; i < seq.size(); ++i) { int a = seq.get(i - 1) - 1, b = seq.get(i) - 1; g[a].add(b); indeg[b]++; } } Deque<Integer> q = new ArrayDeque<>(); for (int i = 0; i < n; ++i) { if (indeg[i] == 0) { q.offer(i); } } while (!q.isEmpty()) { if (q.size() > 1) { return false; } int i = q.poll(); for (int j : g[i]) { if (--indeg[j] == 0) { q.offer(j); } } } return true; } }
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
示例1:

输入:l1 = [7,2,4,3], l2 = [5,6,4] 输出:[7,8,0,7]
示例2:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[8,0,7]
示例3:
输入:l1 = [0], l2 = [0] 输出:[0]
提示:
[1, 100]0 <= node.val <= 9进阶:如果输入链表不能翻转该如何解决?
方法一:翻转
手动翻转链表 l1 与 l2,将此题转换为 2. 两数相加,相加过程一致。对于最后返回的结果链表也需要进行翻转,共计三次。
方法二:栈
进阶条件限制,不可翻转。可以利用两个栈,分别存储两个链表元素。然后对两个栈中对应元素相加,并记录进位 carry。
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { Deque<Integer> s1 = new ArrayDeque<>(); Deque<Integer> s2 = new ArrayDeque<>(); for (; l1 != null; l1 = l1.next) { s1.push(l1.val); } for (; l2 != null; l2 = l2.next) { s2.push(l2.val); } int carry = 0; ListNode dummy = new ListNode(); while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) { carry += (s1.isEmpty() ? 0 : s1.pop()) + (s2.isEmpty() ? 0 : s2.pop()); ListNode node = new ListNode(carry % 10, dummy.next); dummy.next = node; carry /= 10; } return dummy.next; } }
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
[1, 3, 5, 7, 9]、[7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。[1, 1, 2, 5, 7] 不是等差序列。数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。
[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。题目数据保证答案是一个 32-bit 整数。
示例 1:
输入:nums = [2,4,6,8,10] 输出:7 解释:所有的等差子序列为: [2,4,6] [4,6,8] [6,8,10] [2,4,6,8] [4,6,8,10] [2,4,6,8,10] [2,6,10]
示例 2:
输入:nums = [7,7,7,7,7] 输出:16 解释:数组中的任意子序列都是等差子序列。
提示:
1 <= nums.length <= 1000-231 <= nums[i] <= 231 - 1给定平面上 n 对 互不相同 的点 points ,其中 points[i] = [xi, yi] 。回旋镖 是由点 (i, j, k) 表示的元组 ,其中 i 和 j 之间的距离和 i 和 k 之间的欧式距离相等(需要考虑元组的顺序)。
返回平面上所有回旋镖的数量。
示例 1:
输入:points = [[0,0],[1,0],[2,0]] 输出:2 解释:两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]
示例 2:
输入:points = [[1,1],[2,2],[3,3]] 输出:2
示例 3:
输入:points = [[1,1]] 输出:0
提示:
n == points.length1 <= n <= 500points[i].length == 2-104 <= xi, yi <= 104计数器实现。
对于每个点,计算其他点到该点的距离,然后按照距离进行分组计数。对每个组中的点进行两两排列组合(A n 取 2,即 n * (n - 1)))计数即可。
class Solution { public int numberOfBoomerangs(int[][] points) { int ans = 0; for (int[] p : points) { Map<Integer, Integer> counter = new HashMap<>(); for (int[] q : points) { int distance = (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]); counter.put(distance, counter.getOrDefault(distance, 0) + 1); } for (int val : counter.values()) { ans += val * (val - 1); } } return ans; } }
给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1] 输出:[5,6]
示例 2:
输入:nums = [1,1] 输出:[2]
提示:
n == nums.length1 <= n <= 1051 <= nums[i] <= n进阶:你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
方法一:数组或哈希表
我们可以使用数组或哈希表记录数组中的数字,然后遍历 [1, n] 区间内的数字,若数字不存在于数组或哈希表中,则说明数组中缺失该数字,将其添加到结果列表中。
时间复杂度 ,空间复杂度 。其中 为数组长度。
方法二:原地修改
我们可以遍历数组 ,将 位置的数字标记为负数,表示数组 出现过。最后遍历数组 ,若 为正数,则说明数组中缺失 ,将其添加到结果列表中。
遍历结束后,返回结果列表即可。
时间复杂度 ,空间复杂度 。其中 为数组长度。
class Solution { public List<Integer> findDisappearedNumbers(int[] nums) { int n = nums.length; boolean[] s = new boolean[n + 1]; for (int x : nums) { s[x] = true; } List<Integer> ans = new ArrayList<>(); for (int i = 1; i <= n; i++) { if (!s[i]) { ans.add(i); } } return ans; } }
class Solution { public List<Integer> findDisappearedNumbers(int[] nums) { int n = nums.length; for (int x : nums) { int i = Math.abs(x) - 1; if (nums[i] > 0) { nums[i] *= -1; } } List<Integer> ans = new ArrayList<>(); for (int i = 0; i < n; i++) { if (nums[i] > 0) { ans.add(i + 1); } } return ans; } }
序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。
设计一个算法来序列化和反序列化 二叉搜索树 。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。
编码的字符串应尽可能紧凑。
示例 1:
输入:root = [2,1,3] 输出:[2,1,3]
示例 2:
输入:root = [] 输出:[]
提示:
[0, 104]0 <= Node.val <= 104public class Codec { // Encodes a tree to a single string. public String serialize(TreeNode root) { if (root == null) { return ""; } StringBuilder sb = new StringBuilder(); dfs(root, sb); return sb.substring(0, sb.length() - 1); } private void dfs(TreeNode root, StringBuilder sb) { if (root == null) { return; } sb.append(root.val).append(","); dfs(root.left, sb); dfs(root.right, sb); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data == null || "".equals(data)) { return null; } String[] s = data.split(","); return build(s, 0, s.length - 1); } private TreeNode build(String[] s, int l, int r) { if (l > r) { return null; } int idx = r + 1; TreeNode root = new TreeNode(Integer.valueOf(s[l])); for (int i = l + 1; i <= r; ++i) { if (Integer.valueOf(s[i]) > root.val) { idx = i; break; } } root.left = build(s, l + 1, idx - 1); root.right = build(s, idx, r); return root; } } // Your Codec object will be instantiated and called as such: // Codec ser = new Codec(); // Codec deser = new Codec(); // String tree = ser.serialize(root); // TreeNode ans = deser.deserialize(tree); // return ans;
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
示例 1:

输入:root = [5,3,6,2,4,null,7], key = 3 输出:[5,4,6,2,null,null,7] 解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 另一个正确答案是 [5,2,6,null,4,null,7]。![]()
示例 2:
输入: root = [5,3,6,2,4,null,7], key = 0 输出: [5,3,6,2,4,null,7] 解释: 二叉树不包含值为 0 的节点
示例 3:
输入: root = [], key = 0 输出: []
提示:
[0, 104].-105 <= Node.val <= 105root 是合法的二叉搜索树-105 <= key <= 105进阶: 要求算法时间复杂度为 O(h),h 为树的高度。
方法一:递归
二叉搜索树有以下性质:
我们可以递归判断当前节点 与 的大小关系:
时间复杂度 ,其中 是树的高度。
class Solution { public TreeNode deleteNode(TreeNode root, int key) { if (root == null) { return null; } if (root.val > key) { root.left = deleteNode(root.left, key); return root; } if (root.val < key) { root.right = deleteNode(root.right, key); return root; } if (root.left == null) { return root.right; } if (root.right == null) { return root.left; } TreeNode node = root.right; while (node.left != null) { node = node.left; } node.left = root.left; root = root.right; return root; } }
给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。
返回 已排序的字符串 。如果有多个答案,返回其中任何一个。
示例 1:
输入: s = "tree" 输出: "eert" 解释: 'e'出现两次,'r'和't'都只出现一次。 因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
示例 2:
输入: s = "cccaaa" 输出: "cccaaa" 解释: 'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 注意"cacaca"是不正确的,因为相同的字母必须放在一起。
示例 3:
输入: s = "Aabb" 输出: "bbAa" 解释: 此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。 注意'A'和'a'被认为是两种不同的字符。
提示:
1 <= s.length <= 5 * 105s 由大小写英文字母和数字组成方法一:哈希表 + 排序
我们用哈希表 统计字符串 中每个字符出现的次数,然后将 中的键值对按照出现次数降序排序,最后按照排序后的顺序拼接字符串即可。
时间复杂度 ,空间复杂度 ,其中 为字符串 的长度,而 为不同字符的个数。
class Solution { public String frequencySort(String s) { Map<Character, Integer> cnt = new HashMap<>(52); for (int i = 0; i < s.length(); ++i) { cnt.merge(s.charAt(i), 1, Integer::sum); } List<Character> cs = new ArrayList<>(cnt.keySet()); cs.sort((a, b) -> cnt.get(b) - cnt.get(a)); StringBuilder ans = new StringBuilder(); for (char c : cs) { for (int v = cnt.get(c); v > 0; --v) { ans.append(c); } } return ans.toString(); } }
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:气球可以用2支箭来爆破: -在x = 6处射出箭,击破气球[2,8]和[1,6]。 -在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]] 输出:4 解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]] 输出:2 解释:气球可以用2支箭来爆破: - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 - 在x = 4处射出箭,击破气球[3,4]和[4,5]。
提示:
1 <= points.length <= 105points[i].length == 2-231 <= xstart < xend <= 231 - 1方法一:排序 + 贪心
我们可以将所有气球按照右端点升序排序,然后从左到右遍历气球,维护当前的箭所能覆盖的最右端点 ,如果当前气球的左端点 大于 ,说明当前箭无法覆盖当前气球,需要额外射出一支箭,那么答案加一,同时更新 为当前气球的右端点 。
遍历结束后,即可得到答案。
时间复杂度 ,空间复杂度 。其中 为气球的数量。
相似题目:757. 设置交集大小至少为 2
class Solution { public int findMinArrowShots(int[][] points) { // 直接 a[1] - b[1] 可能会溢出 Arrays.sort(points, Comparator.comparingInt(a -> a[1])); int ans = 0; long last = -(1L << 60); for (var p : points) { int a = p[0], b = p[1]; if (a > last) { ++ans; last = b; } } return ans; } }
给你一个长度为 n 的整数数组,每次操作将会使 n - 1 个元素增加 1 。返回让数组所有元素相等的最小操作次数。
示例 1:
输入:nums = [1,2,3] 输出:3 解释: 只需要3次操作(注意每次操作会增加两个元素的值): [1,2,3] => [2,3,3] => [3,4,3] => [4,4,4]
示例 2:
输入:nums = [1,1,1] 输出:0
提示:
n == nums.length1 <= nums.length <= 105-109 <= nums[i] <= 109方法一:数学
我们不妨记数组 的最小值为 ,数组的和为 ,数组的长度为 。
假设最小操作次数为 ,最终数组的所有元素都为 ,则有:
将第二个式子代入第一个式子,得到:
因此,最小操作次数为 。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { public int minMoves(int[] nums) { return Arrays.stream(nums).sum() - Arrays.stream(nums).min().getAsInt() * nums.length; } }
class Solution { public int minMoves(int[] nums) { int s = 0; int mi = 1 << 30; for (int x : nums) { s += x; mi = Math.min(mi, x); } return s - mi * nums.length; } }
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < nnums1[i] + nums2[j] + nums3[k] + nums4[l] == 0示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1
提示:
n == nums1.lengthn == nums2.lengthn == nums3.lengthn == nums4.length1 <= n <= 200-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228方法一:哈希表
我们可以将数组 和 中的元素 和 相加,将所有可能的和存储在哈希表 中,其中键为两数之和,值为两数之和出现的次数。
然后我们遍历数组 和 中的元素 和 ,令 为目标值,那么答案即为 的累加和。
时间复杂度 ,空间复杂度 ,其中 是数组的长度。
class Solution { public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) { Map<Integer, Integer> cnt = new HashMap<>(); for (int a : nums1) { for (int b : nums2) { cnt.merge(a + b, 1, Integer::sum); } } int ans = 0; for (int c : nums3) { for (int d : nums4) { ans += cnt.getOrDefault(-(c + d), 0); } } return ans; } }
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2.
提示:
1 <= g.length <= 3 * 1040 <= s.length <= 3 * 1041 <= g[i], s[j] <= 231 - 1方法一:排序 + 双指针
根据题目描述,我们应该优先将饼干分配给胃口值小的孩子,这样可以尽可能满足更多的孩子。
因此,我们首先对两个数组进行排序,然后用两个指针 和 分别指向数组 和 的头部,每次比较 和 的大小:
如果遍历完数组 ,则说明所有孩子都已经分配到饼干,则返回孩子总数即可。
时间复杂度 ,空间复杂度 。其中 和 分别为数组 和 的长度。
class Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); Arrays.sort(s); int m = g.length; int n = s.length; for (int i = 0, j = 0; i < m; ++i) { while (j < n && s[j] < g[i]) { ++j; } if (j++ >= n) { return i; } } return m; } }
给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。
如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4] 输出:false 解释:序列中不存在 132 模式的子序列。
示例 2:
输入:nums = [3,1,4,2] 输出:true 解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。
示例 3:
输入:nums = [-1,3,2,0] 输出:true 解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。
提示:
n == nums.length1 <= n <= 2 * 105-109 <= nums[i] <= 109方法一:单调栈
我们可以枚举从右往左枚举整数 ,并维护一个单调栈,栈中的元素从栈底到栈顶单调递减。维护一个变量 ,表示 右侧且小于 的最大值。初始时, 的值为 。
我们从右往左遍历数组,对于遍历到的每个元素 ,如果 小于 ,则说明我们找到了一个满足 的三元组,返回 true。否则,如果栈顶元素小于 ,则我们循环将栈顶元素出栈,并且更新 的值为出栈元素,直到栈为空或者栈顶元素大于等于 。最后,我们将 入栈。
如果遍历结束后仍未找到满足条件的三元组,说明不存在这样的三元组,返回 false。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
方法二:离散化 + 树状数组
我们可以用树状数组维护比某个数小的元素的个数,用一个数组 记录 左侧的最小值。
我们从右往左遍历数组,对于遍历到的每个元素 ,我们将 离散化为一个整数 ,将 离散化为一个整数 ,如果此时 ,并且树状数组中存在比 大但比 小的元素,则说明存在满足 的三元组,返回 true。否则,我们将 的离散化结果 更新到树状数组中。
如果遍历结束后仍未找到满足条件的三元组,说明不存在这样的三元组,返回 false。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
class Solution { public boolean find132pattern(int[] nums) { int vk = -(1 << 30); Deque<Integer> stk = new ArrayDeque<>(); for (int i = nums.length - 1; i >= 0; --i) { if (nums[i] < vk) { return true; } while (!stk.isEmpty() && stk.peek() < nums[i]) { vk = stk.pop(); } stk.push(nums[i]); } return false; } }
class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int v) { while (x <= n) { c[x] += v; x += x & -x; } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= x & -x; } return s; } } class Solution { public boolean find132pattern(int[] nums) { int[] s = nums.clone(); Arrays.sort(s); int n = nums.length; int m = 0; int[] left = new int[n + 1]; left[0] = 1 << 30; for (int i = 0; i < n; ++i) { left[i + 1] = Math.min(left[i], nums[i]); if (i == 0 || s[i] != s[i - 1]) { s[m++] = s[i]; } } BinaryIndexedTree tree = new BinaryIndexedTree(m); for (int i = n - 1; i >= 0; --i) { int x = search(s, m, nums[i]); int y = search(s, m, left[i]); if (x > y && tree.query(x - 1) > tree.query(y)) { return true; } tree.update(x, 1); } return false; } private int search(int[] nums, int r, int x) { int l = 0; while (l < r) { int mid = (l + r) >> 1; if (nums[mid] >= x) { r = mid; } else { l = mid + 1; } } return l + 1; } }
存在一个不含 0 的 环形 数组 nums ,每个 nums[i] 都表示位于下标 i 的角色应该向前或向后移动的下标个数:
nums[i] 是正数,向前(下标递增方向)移动 |nums[i]| 步nums[i] 是负数,向后(下标递减方向)移动 |nums[i]| 步因为数组是 环形 的,所以可以假设从最后一个元素向前移动一步会到达第一个元素,而第一个元素向后移动一步会到达最后一个元素。
数组中的 循环 由长度为 k 的下标序列 seq 标识:
seq[0] -> seq[1] -> ... -> seq[k - 1] -> seq[0] -> ...nums[seq[j]] 应当不是 全正 就是 全负k > 1如果 nums 中存在循环,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,-1,1,2,2] 输出:true 解释:存在循环,按下标 0 -> 2 -> 3 -> 0 。循环长度为 3 。
示例 2:
输入:nums = [-1,2] 输出:false 解释:按下标 1 -> 1 -> 1 ... 的运动无法构成循环,因为循环的长度为 1 。根据定义,循环的长度必须大于 1 。
示例 3:
输入:nums = [-2,1,-1,-2,-2] 输出:false 解释:按下标 1 -> 2 -> 1 -> ... 的运动无法构成循环,因为 nums[1] 是正数,而 nums[2] 是负数。 所有 nums[seq[j]] 应当不是全正就是全负。
提示:
1 <= nums.length <= 5000-1000 <= nums[i] <= 1000nums[i] != 0进阶:你能设计一个时间复杂度为 O(n) 且额外空间复杂度为 O(1) 的算法吗?
快慢指针。
class Solution { private int n; private int[] nums; public boolean circularArrayLoop(int[] nums) { n = nums.length; this.nums = nums; for (int i = 0; i < n; ++i) { if (nums[i] == 0) { continue; } int slow = i, fast = next(i); while (nums[slow] * nums[fast] > 0 && nums[slow] * nums[next(fast)] > 0) { if (slow == fast) { if (slow != next(slow)) { return true; } break; } slow = next(slow); fast = next(next(fast)); } int j = i; while (nums[j] * nums[next(j)] > 0) { nums[j] = 0; j = next(j); } } return false; } private int next(int i) { return (i + nums[i] % n + n) % n; } }
有 buckets 桶液体,其中 正好有一桶 含有毒药,其余装的都是水。它们从外观看起来都一样。为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 minutesToTest 分钟时间来确定哪桶液体是有毒的。
喂猪的规则如下:
minutesToDie 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。minutesToDie 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。给你桶的数目 buckets ,minutesToDie 和 minutesToTest ,返回 在规定时间内判断哪个桶有毒所需的 最小 猪数 。
示例 1:
输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60 输出:5
示例 2:
输入:buckets = 4, minutesToDie = 15, minutesToTest = 15 输出:2
示例 3:
输入:buckets = 4, minutesToDie = 15, minutesToTest = 30 输出:2
提示:
1 <= buckets <= 10001 <= minutesToDie <= minutesToTest <= 100每只 🐖 可以喝液体的次数是 minutesToTest / minutesToDie,那么 🐖 会有 (minutesToTest / minutesToDie) + 1 种状态,即喝完第 1 次死亡,喝完第 2 次死亡,...,喝完第 minutesToTest / minutesToDie 死亡,喝完第 minutesToTest / minutesToDie 次依然存活。
我们设定 base = (minutesToTest / minutesToDie) + 1,n 只 🐖 能验证的范围是 pow(base, n),因此求 pow(base, n) >= buckets 的最小 n 即可。
class Solution { public int poorPigs(int buckets, int minutesToDie, int minutesToTest) { int base = minutesToTest / minutesToDie + 1; int res = 0; for (int p = 1; p < buckets; p *= base) { ++res; } return res; } }
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。
示例 2:
输入: s = "aba" 输出: false
示例 3:
输入: s = "abcabcabcabc" 输出: true 解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
提示:
1 <= s.length <= 104s 由小写英文字母组成方法一:双倍字符串
若长度为 的字符串 s 由 个重复子串组成,将 s 拼接在自身上,得到字符串 ss,长度为 ,此时若从下标 1 开始查找 s,那么查找到的下标一定小于 s.length。
若长度为 的字符串 s 不由重复子串组成,将 s 拼接在自身上,得到字符串 ss,长度为 ,此时若从下标 1 开始查找 s,那么查找到的下标一定等于 s.length。
class Solution { public boolean repeatedSubstringPattern(String s) { String str = s + s; return str.substring(1, str.length() - 1).contains(s); } }
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入:
["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]
解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1); // cache=[1,_], cnt(1)=1
lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1); // 返回 1
// cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
// cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2); // 返回 -1(未找到)
lfu.get(3); // 返回 3
// cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
// cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1); // 返回 -1(未找到)
lfu.get(3); // 返回 3
// cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4); // 返回 4
// cache=[3,4], cnt(4)=2, cnt(3)=3
提示:
0 <= capacity <= 1040 <= key <= 1050 <= value <= 1092 * 105 次 get 和 put 方法和 LRU 缓存 类似的思路,用 map<key, node> 和 map<freq, list<node>> 存储不同使用频率的节点
对于 get 操作,判断 key 是否存在哈希表中:
对于 put 操作,同样判断 key 是否存在哈希表中:
class LFUCache { private final Map<Integer, Node> map; private final Map<Integer, DoublyLinkedList> freqMap; private final int capacity; private int minFreq; public LFUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(capacity, 1); freqMap = new HashMap<>(); } public int get(int key) { if (capacity == 0) { return -1; } if (!map.containsKey(key)) { return -1; } Node node = map.get(key); incrFreq(node); return node.value; } public void put(int key, int value) { if (capacity == 0) { return; } if (map.containsKey(key)) { Node node = map.get(key); node.value = value; incrFreq(node); return; } if (map.size() == capacity) { DoublyLinkedList list = freqMap.get(minFreq); map.remove(list.removeLast().key); } Node node = new Node(key, value); addNode(node); map.put(key, node); minFreq = 1; } private void incrFreq(Node node) { int freq = node.freq; DoublyLinkedList list = freqMap.get(freq); list.remove(node); if (list.isEmpty()) { freqMap.remove(freq); if (freq == minFreq) { minFreq++; } } node.freq++; addNode(node); } private void addNode(Node node) { int freq = node.freq; DoublyLinkedList list = freqMap.getOrDefault(freq, new DoublyLinkedList()); list.addFirst(node); freqMap.put(freq, list); } private static class Node { int key; int value; int freq; Node prev; Node next; Node(int key, int value) { this.key = key; this.value = value; this.freq = 1; } } private static class DoublyLinkedList { private final Node head; private final Node tail; public DoublyLinkedList() { head = new Node(-1, -1); tail = new Node(-1, -1); head.next = tail; tail.prev = head; } public void addFirst(Node node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } public Node remove(Node node) { node.next.prev = node.prev; node.prev.next = node.next; node.next = null; node.prev = null; return node; } public Node removeLast() { return remove(tail.prev); } public boolean isEmpty() { return head.next == tail; } } }
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
示例 1:
输入:x = 1, y = 4
输出:2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
示例 2:
输入:x = 3, y = 1 输出:1
提示:
0 <= x, y <= 231 - 1方法一:位运算
我们将 和 按位异或,得到的结果中的 的个数就是汉明距离。
时间复杂度 ,空间复杂度 。
class Solution { public int hammingDistance(int x, int y) { return Integer.bitCount(x ^ y); } }
给你一个长度为 n 的整数数组 nums ,返回使所有数组元素相等需要的最小操作数。
在一次操作中,你可以使数组中的一个元素加 1 或者减 1 。
示例 1:
输入:nums = [1,2,3] 输出:2 解释: 只需要两次操作(每次操作指南使一个元素加 1 或减 1): [1,2,3] => [2,2,3] => [2,2,2]
示例 2:
输入:nums = [1,10,2,9] 输出:16
提示:
n == nums.length1 <= nums.length <= 105-109 <= nums[i] <= 109方法一:排序 + 中位数
这个问题可以抽象为,在数轴上有 个点,找到一个点使得所有点到该点的距离之和最小。答案为 个点的中位数。
中位数有这样的性质:所有数与中位数的距离之和最小。
证明:
首先,给定一个从小到大的数列 ,我们假设 是从 到 与其距离之和最小的点,显然 必须位于 和 之间。而由于 与 与 的距离之和都相等,都等于 ,因此,接下来不考虑 和 ,我们只考虑 ,这样的话,我们就可以把问题转化为在 中找到一个点与其距离之和最小,依此类推,我们最后可以得出结论:在一个数列中,中位数与其余数的距离之和最小。
在这个问题中,我们可以先对数组进行排序,然后找到中位数,最后计算所有数与中位数的距离之和即可。
时间复杂度 ,空间复杂度 。
相似题目:
方法二:排序 + 前缀和
如果我们不知道中位数的性质,也可以使用前缀和的方法来求解。
时间复杂度 ,空间复杂度 。
class Solution { public int minMoves2(int[] nums) { Arrays.sort(nums); int k = nums[nums.length >> 1]; int ans = 0; for (int v : nums) { ans += Math.abs(v - k); } return ans; } }
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例 1:

输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]] 输出:16 解释:它的周长是上面图片中的 16 个黄色的边
示例 2:
输入:grid = [[1]] 输出:4
示例 3:
输入:grid = [[1,0]] 输出:4
提示:
row == grid.lengthcol == grid[i].length1 <= row, col <= 100grid[i][j] 为 0 或 1遍历二维数组
class Solution { public int islandPerimeter(int[][] grid) { int ans = 0; int m = grid.length; int n = grid[0].length; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) { ans += 4; if (i < m - 1 && grid[i + 1][j] == 1) { ans -= 2; } if (j < n - 1 && grid[i][j + 1] == 1) { ans -= 2; } } } } return ans; } }
在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和 达到或超过 100 的玩家,即为胜者。
如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?
例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。
示例 1:
输入:maxChoosableInteger = 10, desiredTotal = 11 输出:false 解释: 无论第一个玩家选择哪个整数,他都会失败。 第一个玩家可以选择从 1 到 10 的整数。 如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。 第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利. 同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
示例 2:
输入:maxChoosableInteger = 10, desiredTotal = 0 输出:true
示例 3:
输入:maxChoosableInteger = 10, desiredTotal = 1 输出:true
提示:
1 <= maxChoosableInteger <= 200 <= desiredTotal <= 300方法一:状态压缩 + 记忆化搜索
class Solution { private Map<Integer, Boolean> memo = new HashMap<>(); public boolean canIWin(int maxChoosableInteger, int desiredTotal) { int s = (1 + maxChoosableInteger) * maxChoosableInteger / 2; if (s < desiredTotal) { return false; } return dfs(0, 0, maxChoosableInteger, desiredTotal); } private boolean dfs(int state, int t, int maxChoosableInteger, int desiredTotal) { if (memo.containsKey(state)) { return memo.get(state); } boolean res = false; for (int i = 1; i <= maxChoosableInteger; ++i) { if (((state >> i) & 1) == 0) { if (t + i >= desiredTotal || !dfs(state | 1 << i, t + i, maxChoosableInteger, desiredTotal)) { res = true; break; } } } memo.put(state, res); return res; } }
给你一个表示交易的数组 transactions ,其中 transactions[i] = [fromi, toi, amounti] 表示 ID = fromi 的人给 ID = toi 的人共计 amounti $ 。
请你计算并返回还清所有债务的最小交易笔数。
示例 1:
输入:transactions = [[0,1,10],[2,0,5]] 输出:2 解释: #0 给 #1 $10 。 #2 给 #0 $5 。 需要进行两笔交易。一种结清债务的方式是 #1 给 #0 和 #2 各 $5 。
示例 2:
输入:transactions = [[0,1,10],[1,0,1],[1,2,5],[2,0,5]] 输出:1 解释: #0 给 #1 $10 。 #1 给 #0 $1 。 #1 给 #2 $5 。 #2 给 #0 $5 。 因此,#1 只需要给 #0 $4 ,所有的债务即可还清。
提示:
1 <= transactions.length <= 8transactions[i].length == 30 <= fromi, toi < 12fromi != toi1 <= amounti <= 100方法一:状态压缩动态规划 + 子集枚举
我们先遍历数组 transactions,统计每个人的收支情况,然后将所有收支不为零的人的收支情况存入数组 中。如果我们可以找到一个子集,子集中共有 个人,且这 个人的收支情况之和为零,那么我们最多通过 次交易,就能够使得这 个人的收支情况全部清零。这样,我们就能将原问题转化成一个子集枚举的问题。
我们定义 表示将集合 的所有元素的收支情况全部清零,所需的最少交易次数,初始时 ,其余 。
考虑 ,其中 , 是数组 的长度。我们可以统计集合 中所有元素的收支情况之和 ,如果 ,那么 的取值不超过 ,其中 表示集合 中的元素个数。然后我们可以枚举 的所有非空子集 ,计算 ,其中 和 分别表示将集合 和 的所有元素的收支情况全部清零,所需的最少交易次数。我们可以得到状态转移方程:
其中 表示 是 的子集,且 。
最终答案即为 。
时间复杂度 ,空间复杂度 。其中 是人的数量,本题中 。
class Solution { public int minTransfers(int[][] transactions) { int[] g = new int[12]; for (var t : transactions) { g[t[0]] -= t[2]; g[t[1]] += t[2]; } List<Integer> nums = new ArrayList<>(); for (int x : g) { if (x != 0) { nums.add(x); } } int m = nums.size(); int[] f = new int[1 << m]; Arrays.fill(f, 1 << 29); f[0] = 0; for (int i = 1; i < 1 << m; ++i) { int s = 0; for (int j = 0; j < m; ++j) { if ((i >> j & 1) == 1) { s += nums.get(j); } } if (s == 0) { f[i] = Integer.bitCount(i) - 1; for (int j = (i - 1) & i; j > 0; j = (j - 1) & i) { f[i] = Math.min(f[i], f[j] + f[i ^ j]); } } } return f[(1 << m) - 1]; } }
定义 str = [s, n] 表示 str 由 n 个字符串 s 连接构成。
str == ["abc", 3] =="abcabcabc" 。如果可以从 s2 中删除某些字符使其变为 s1,则称字符串 s1 可以从字符串 s2 获得。
s1 = "abc" 可以从 s2 = "abdbec" 获得,仅需要删除加粗且用斜体标识的字符。现在给你两个字符串 s1 和 s2 和两个整数 n1 和 n2 。由此构造得到两个字符串,其中 str1 = [s1, n1]、str2 = [s2, n2] 。
请你找出一个最大整数 m ,以满足 str = [str2, m] 可以从 str1 获得。
示例 1:
输入:s1 = "acb", n1 = 4, s2 = "ab", n2 = 2 输出:2
示例 2:
输入:s1 = "acb", n1 = 1, s2 = "acb", n2 = 1 输出:1
提示:
1 <= s1.length, s2.length <= 100s1 和 s2 由小写英文字母组成1 <= n1, n2 <= 106定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串,所以 base 看起来是这样的:
"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".给你一个字符串 s ,请你统计并返回 s 中有多少 不同非空子串 也在 base 中出现。
示例 1:
输入:s = "a" 输出:1 解释:字符串 s 的子字符串 "a" 在 base 中出现。
示例 2:
输入:s = "cac"
输出:2
解释:字符串 s 有两个子字符串 ("a", "c") 在 base 中出现。
示例 3:
输入:s = "zab"
输出:6
解释:字符串 s 有六个子字符串 ("z", "a", "b", "za", "ab", and "zab") 在 base 中出现。
提示:
1 <= s.length <= 105方法一:动态规划
不妨设 dp[α] 表示 p 中以字符 α 结尾且在 s 中的子串的最大长度,将 dp 求和可以得到最终结果。
时间复杂度 ,其中 表示字符串 p 的长度。
成为子串的一个标准,需要是连续的,
a与c之间少了一个b,所以不能算一个子字符串。
class Solution { public int findSubstringInWraproundString(String p) { int[] dp = new int[26]; int k = 0; for (int i = 0; i < p.length(); ++i) { char c = p.charAt(i); if (i > 0 && (c - p.charAt(i - 1) + 26) % 26 == 1) { ++k; } else { k = 1; } dp[c - 'a'] = Math.max(dp[c - 'a'], k); } int ans = 0; for (int v : dp) { ans += v; } return ans; } }
给定一个字符串 queryIP。如果是有效的 IPv4 地址,返回 "IPv4" ;如果是有效的 IPv6 地址,返回 "IPv6" ;如果不是上述类型的 IP 地址,返回 "Neither" 。
有效的IPv4地址 是 “x1.x2.x3.x4” 形式的IP地址。 其中 0 <= xi <= 255 且 xi 不能包含 前导零。例如: “192.168.1.1” 、 “192.168.1.0” 为有效IPv4地址, “192.168.01.1” 为无效IPv4地址; “192.168.1.00” 、 “192.168@1.1” 为无效IPv4地址。
一个有效的IPv6地址 是一个格式为“x1:x2:x3:x4:x5:x6:x7:x8” 的IP地址,其中:
1 <= xi.length <= 4xi 是一个 十六进制字符串 ,可以包含数字、小写英文字母( 'a' 到 'f' )和大写英文字母( 'A' 到 'F' )。xi 中允许前导零。例如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 和 "2001:db8:85a3:0:0:8A2E:0370:7334" 是有效的 IPv6 地址,而 "2001:0db8:85a3::8A2E:037j:7334" 和 "02001:0db8:85a3:0000:0000:8a2e:0370:7334" 是无效的 IPv6 地址。
示例 1:
输入:queryIP = "172.16.254.1" 输出:"IPv4" 解释:有效的 IPv4 地址,返回 "IPv4"
示例 2:
输入:queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334" 输出:"IPv6" 解释:有效的 IPv6 地址,返回 "IPv6"
示例 3:
输入:queryIP = "256.256.256.256" 输出:"Neither" 解释:既不是 IPv4 地址,又不是 IPv6 地址
提示:
queryIP 仅由英文字母,数字,字符 '.' 和 ':' 组成。给定 X-Y 平面上的一组点 points ,其中 points[i] = [xi, yi] 。这些点按顺序连成一个多边形。
如果该多边形为 凸 多边形(凸多边形的定义)则返回 true ,否则返回 false 。
你可以假设由给定点构成的多边形总是一个 简单的多边形(简单多边形的定义)。换句话说,我们要保证每个顶点处恰好是两条边的汇合点,并且这些边 互不相交 。
示例 1:

输入: points = [[0,0],[0,5],[5,5],[5,0]] 输出: true
示例 2:

输入: points = [[0,0],[0,10],[10,10],[10,0],[5,5]] 输出: false
提示:
3 <= points.length <= 104points[i].length == 2-104 <= xi, yi <= 104给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。
你只能调用 rand7() 且不能调用其他方法。请不要使用系统的 Math.random() 方法。
每个测试用例将有一个内部参数 n,即你实现的函数 rand10() 在测试时将被调用的次数。请注意,这不是传递给 rand10() 的参数。
示例 1:
输入: 1 输出: [2]
示例 2:
输入: 2 输出: [2,8]
示例 3:
输入: 3 输出: [3,8,10]
提示:
1 <= n <= 105进阶:
rand7()调用次数的 期望值 是多少 ?rand7() ?给定一个 非空 字符串,将其编码为具有最短长度的字符串。
编码规则是:k[encoded_string],其中在方括号 encoded_string 中的内容重复 k 次。
注:
示例 1:
输入:s = "aaa" 输出:"aaa" 解释:无法将其编码为更短的字符串,因此不进行编码。
示例 2:
输入:s = "aaaaa" 输出:"5[a]" 解释:"5[a]" 比 "aaaaa" 短 1 个字符。
示例 3:
输入:s = "aaaaaaaaaa" 输出:"10[a]" 解释:"a9[a]" 或 "9[a]a" 都是合法的编码,和 "10[a]" 一样长度都为 5。
示例 4:
输入:s = "aabcaabcd" 输出:"2[aabc]d" 解释:"aabc" 出现两次,因此一种答案可以是 "2[aabc]d"。
示例 5:
输入:s = "abbbabbbcabbbabbbc" 输出:"2[2[abbb]c]" 解释:"abbbabbbc" 出现两次,但是 "abbbabbbc" 可以编码为 "2[abbb]c",因此一种答案可以是 "2[2[abbb]c]"。
提示:
1 <= s.length <= 150s 由小写英文字母组成给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 。
连接词 定义为:一个完全由给定数组中的至少两个较短单词(不一定是不同的两个单词)组成的字符串。
示例 1:
输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
"dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
"ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。
示例 2:
输入:words = ["cat","dog","catdog"] 输出:["catdog"]
提示:
1 <= words.length <= 1041 <= words[i].length <= 30words[i] 仅由小写英文字母组成。words 中的所有字符串都是 唯一 的。1 <= sum(words[i].length) <= 105方法一:前缀树 + DFS
判断一个单词是不是连接词,需要判断这个单词是否完全由至少两个给定数组中的更短的非空单词(可以重复)组成。判断更短的单词是否在给定数组中,可以使用字典树实现。
首先将 按照字符串的长度递增的顺序排序,排序后可以确保当遍历到任意单词时,比该单词短的单词一定都已经遍历过,因此可以根据已经遍历过的全部单词判断当前单词是不是连接词。
在将 排序之后,遍历 ,跳过空字符串,对于每个非空单词,判断该单词是不是连接词,如果是连接词则将该单词加入结果数组,如果不是连接词则将该单词加入字典树。
判断一个单词是不是连接词的做法是在字典树中深度优先搜索。从该单词的第一个字符(即下标 处的字符)开始,在字典树中依次搜索每个字符对应的结点,可能有以下几种情况:
如果找到一个更短的单词且这个更短的单词的最后一个字符是当前单词的最后一个字符,则当前单词是连接词。由于数组 中没有重复的单词,因此在判断一个单词是不是连接词时,该单词一定没有加入字典树,由此可以确保判断连接词的条件成立。
说明:由于一个连接词由多个更短的非空单词组成,如果存在一个较长的连接词的组成部分之一是一个较短的连接词,则一定可以将这个较短的连接词换成多个更短的非空单词,因此不需要将连接词加入字典树。
class Trie { Trie[] children = new Trie[26]; boolean isEnd; void insert(String w) { Trie node = this; for (char c : w.toCharArray()) { c -= 'a'; if (node.children[c] == null) { node.children[c] = new Trie(); } node = node.children[c]; } node.isEnd = true; } } class Solution { private Trie trie = new Trie(); public List<String> findAllConcatenatedWordsInADict(String[] words) { Arrays.sort(words, (a, b) -> a.length() - b.length()); List<String> ans = new ArrayList<>(); for (String w : words) { if (dfs(w)) { ans.add(w); } else { trie.insert(w); } } return ans; } private boolean dfs(String w) { if ("".equals(w)) { return true; } Trie node = trie; for (int i = 0; i < w.length(); ++i) { int idx = w.charAt(i) - 'a'; if (node.children[idx] == null) { return false; } node = node.children[idx]; if (node.isEnd && dfs(w.substring(i + 1))) { return true; } } return false; } }
你将得到一个整数数组 matchsticks ,其中 matchsticks[i] 是第 i 个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。
如果你能使这个正方形,则返回 true ,否则返回 false 。
示例 1:

输入: matchsticks = [1,1,2,2,2] 输出: true 解释: 能拼成一个边长为2的正方形,每边两根火柴。
示例 2:
输入: matchsticks = [3,3,3,3,4] 输出: false 解释: 不能用所有火柴拼成一个正方形。
提示:
1 <= matchsticks.length <= 151 <= matchsticks[i] <= 108方法一:排序 + 回溯
用 记录正方形每条边当前的长度,对于第 根火柴,尝试把它加到 每条边,若加入后 不超过正方形期望长度 ,则继续往下递归 根火柴。若所有火柴都能被加入,说明满足拼成正方形的要求。
这里对 从大到小排序,可以减少搜索次数。
时间复杂度 ,其中 表示 的长度。每根火柴可以被放入正方形的 条边,共有 根火柴。
方法二:状态压缩 + 记忆化搜索
记当前火柴被划分的情况为 。对于第 个数,若 ,说明第 个火柴棒未被划分。我们的目标是从全部数字中凑出 个和为 的子集。
记当前子集的和为 。在未划分第 个火柴棒时:
注:若 ,说明恰好可以得到一个和为 的子集,下一步将 归零(可以通过 实现),并继续划分下一个子集。
class Solution { public boolean makesquare(int[] matchsticks) { int s = 0, mx = 0; for (int v : matchsticks) { s += v; mx = Math.max(mx, v); } int x = s / 4, mod = s % 4; if (mod != 0 || x < mx) { return false; } Arrays.sort(matchsticks); int[] edges = new int[4]; return dfs(matchsticks.length - 1, x, matchsticks, edges); } private boolean dfs(int u, int x, int[] matchsticks, int[] edges) { if (u < 0) { return true; } for (int i = 0; i < 4; ++i) { if (i > 0 && edges[i - 1] == edges[i]) { continue; } edges[i] += matchsticks[u]; if (edges[i] <= x && dfs(u - 1, x, matchsticks, edges)) { return true; } edges[i] -= matchsticks[u]; } return false; } }
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
1 <= strs.length <= 6001 <= strs[i].length <= 100strs[i] 仅由 '0' 和 '1' 组成1 <= m, n <= 100题目可以转换为 0-1 背包问题,在 k 个字符串中选出一些字符串(每个字符串只能使用一次),并且满足字符串最多包含 m 个 0 和 n 个 1,求满足此条件的字符串的最大长度(字符串个数)。
空间优化:
class Solution { public : int findMaxForm(vector<string>& strs, int m, int n) { vector<vector<int>> dp(m + 1, vector<int>(n + 1)); for (auto s : strs) { vector<int> t = count(s); for (int i = m; i >= t[0]; --i) for (int j = n; j >= t[1]; --j) dp[i][j] = max(dp[i][j], dp[i - t[0]][j - t[1]] + 1); } return dp[m][n]; } vector<int> count(string s) { int n0 = 0; for (char c : s) if (c == '0') ++n0; return {n0, (int) s.size() - n0}; } };
冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。
在加热器的加热半径范围内的每个房屋都可以获得供暖。
现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。
说明:所有供暖器都遵循你的半径标准,加热的半径也一样。
示例 1:
输入: houses = [1,2,3], heaters = [2] 输出: 1 解释: 仅在位置2上有一个供暖器。如果我们将加热半径设为1,那么所有房屋就都能得到供暖。
示例 2:
输入: houses = [1,2,3,4], heaters = [1,4] 输出: 1 解释: 在位置1, 4上有两个供暖器。我们需要将加热半径设为1,这样所有房屋就都能得到供暖。
示例 3:
输入:houses = [1,5], heaters = [2] 输出:3
提示:
1 <= houses.length, heaters.length <= 3 * 1041 <= houses[i], heaters[i] <= 109排序 + 二分查找 + 双指针。
class Solution { public int findRadius(int[] houses, int[] heaters) { Arrays.sort(heaters); int res = 0; for (int x : houses) { int i = Arrays.binarySearch(heaters, x); if (i < 0) { i = ~i; } int dis1 = i > 0 ? x - heaters[i - 1] : Integer.MAX_VALUE; int dis2 = i < heaters.length ? heaters[i] - x : Integer.MAX_VALUE; res = Math.max(res, Math.min(dis1, dis2)); } return res; } }
对整数的二进制表示取反(0 变 1 ,1 变 0)后,再转换为十进制表示,可以得到这个整数的补数。
5 的二进制表示是 "101" ,取反后得到 "010" ,再转回十进制表示得到补数 2 。给你一个整数 num ,输出它的补数。
示例 1:
输入:num = 5 输出:2 解释:5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。
示例 2:
输入:num = 1 输出:0 解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。
提示:
1 <= num < 231注意:本题与 1009 https://leetcode.cn/problems/complement-of-base-10-integer/ 相同
class Solution { public int findComplement(int num) { int ans = 0; boolean find = false; for (int i = 30; i >= 0; --i) { int b = num & (1 << i); if (!find && b == 0) { continue; } find = true; if (b == 0) { ans |= (1 << i); } } return ans; } }
两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。
给你一个整数数组 nums,请你计算并返回 nums 中任意两个数之间 汉明距离的总和 。
示例 1:
输入:nums = [4,14,2] 输出:6 解释:在二进制表示中,4 表示为 0100 ,14 表示为 1110 ,2表示为 0010 。(这样表示是为了体现后四位之间关系) 所以答案为: HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6
示例 2:
输入:nums = [4,14,4] 输出:4
提示:
1 <= nums.length <= 1040 <= nums[i] <= 109方法一:位运算
class Solution { public int totalHammingDistance(int[] nums) { int ans = 0; for (int i = 0; i < 31; ++i) { int a = 0, b = 0; for (int v : nums) { int t = (v >> i) & 1; a += t; b += t ^ 1; } ans += a * b; } return ans; } }
给定圆的半径和圆心的位置,实现函数 randPoint ,在圆中产生均匀随机点。
实现 Solution 类:
Solution(double radius, double x_center, double y_center) 用圆的半径 radius 和圆心的位置 (x_center, y_center) 初始化对象randPoint() 返回圆内的一个随机点。圆周上的一点被认为在圆内。答案作为数组返回 [x, y] 。示例 1:
输入: ["Solution","randPoint","randPoint","randPoint"] [[1.0, 0.0, 0.0], [], [], []] 输出: [null, [-0.02493, -0.38077], [0.82314, 0.38945], [0.36572, 0.17248]] 解释: Solution solution = new Solution(1.0, 0.0, 0.0); solution.randPoint ();//返回[-0.02493,-0.38077] solution.randPoint ();//返回[0.82314,0.38945] solution.randPoint ();//返回[0.36572,0.17248]
提示:
0 < radius <= 108-107 <= x_center, y_center <= 107randPoint 最多被调用 3 * 104 次给定一个整数 n ,返回 可表示为两个 n 位整数乘积的 最大回文整数 。因为答案可能非常大,所以返回它对 1337 取余 。
示例 1:
输入:n = 2 输出:987 解释:99 x 91 = 9009, 9009 % 1337 = 987
示例 2:
输入: n = 1 输出: 9
提示:
1 <= n <= 8class Solution { public int largestPalindrome(int n) { int mx = (int) Math.pow(10, n) - 1; for (int a = mx; a > mx / 10; --a) { int b = a; long x = a; while (b != 0) { x = x * 10 + b % 10; b /= 10; } for (long t = mx; t * t >= x; --t) { if (x % t == 0) { return (int) (x % 1337); } } } return 9; } }
中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
例如:
[2,3,4],中位数是 3[2,3],中位数是 (2 + 3) / 2 = 2.5给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
示例:
给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。
窗口位置 中位数 --------------- ----- [1 3 -1] -3 5 3 6 7 1 1 [3 -1 -3] 5 3 6 7 -1 1 3 [-1 -3 5] 3 6 7 -1 1 3 -1 [-3 5 3] 6 7 3 1 3 -1 -3 [5 3 6] 7 5 1 3 -1 -3 5 [3 6 7] 6
因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。
提示:
k 始终有效,即:k 始终小于等于输入的非空数组的元素个数。10 ^ -5 以内的答案将被视作正确答案。方法一:双优先队列(大小根堆) + 延迟删除
我们可以使用两个优先队列(大小根堆)维护当前窗口中的元素,其中一个优先队列存储当前窗口中较小的一半元素,另一个优先队列存储当前窗口中较大的一半元素。这样,当前窗口的中位数就是两个优先队列的堆顶元素的平均值或其中的一个。
我们设计一个类 MedianFinder,用于维护当前窗口中的元素。该类包含以下方法:
add_num(num):将 num 加入当前窗口中。find_median():返回当前窗口中元素的中位数。remove_num(num):将 num 从当前窗口中移除。prune(pq):如果堆顶元素在延迟删除字典 delayed 中,则将其从堆顶弹出,并从该元素的延迟删除次数中减一。如果该元素的延迟删除次数为零,则将其从延迟删除字典中删除。rebalance():如果较小的一半元素的数量比较大的一半元素的数量多 2 个,则将较大的一半元素的堆顶元素加入较小的一半元素中;如果较小的一半元素的数量比较大的一半元素的数量少,则将较大的一半元素的堆顶元素加入较小的一半元素中。在 add_num(num) 方法中,我们先考虑将 num 加入较小的一半元素中,如果 num 大于较大的一半元素的堆顶元素,则将 num 加入较大的一半元素中。然后我们调用 rebalance() 方法,使得两个优先队列的大小之差不超过 。
在 remove_num(num) 方法中,我们将 num 的延迟删除次数加一。然后我们将 num 与较小的一半元素的堆顶元素进行比较,如果 num 小于等于较小的一半元素的堆顶元素,则更新较小的一半元素的大小,并且调用 prune() 方法,使得较小的一半元素的堆顶元素不在延迟删除字典中。否则,我们更新较大的一半元素的大小,并且调用 prune() 方法,使得较大的一半元素的堆顶元素不在延迟删除字典中。
在 find_median() 方法中,如果当前窗口的大小为奇数,则返回较小的一半元素的堆顶元素;否则,返回较小的一半元素的堆顶元素与较大的一半元素的堆顶元素的平均值。
在 prune(pq) 方法中,如果堆顶元素在延迟删除字典中,则将其从堆顶弹出,并从该元素的延迟删除次数中减一。如果该元素的延迟删除次数为零,则将其从延迟删除字典中删除。
在 rebalance() 方法中,如果较小的一半元素的数量比较大的一半元素的数量多 2 个,则将较大的一半元素的堆顶元素加入较小的一半元素中;如果较小的一半元素的数量比较大的一半元素的数量少,则将较大的一半元素的堆顶元素加入较小的一半元素中。
时间复杂度 ,空间复杂度 。其中 为数组 nums 的长度。
class MedianFinder { private PriorityQueue<Integer> small = new PriorityQueue<>(Comparator.reverseOrder()); private PriorityQueue<Integer> large = new PriorityQueue<>(); private Map<Integer, Integer> delayed = new HashMap<>(); private int smallSize; private int largeSize; private int k; public MedianFinder(int k) { this.k = k; } public void addNum(int num) { if (small.isEmpty() || num <= small.peek()) { small.offer(num); ++smallSize; } else { large.offer(num); ++largeSize; } rebalance(); } public double findMedian() { return (k & 1) == 1 ? small.peek() : ((double) small.peek() + large.peek()) / 2; } public void removeNum(int num) { delayed.merge(num, 1, Integer::sum); if (num <= small.peek()) { --smallSize; if (num == small.peek()) { prune(small); } } else { --largeSize; if (num == large.peek()) { prune(large); } } rebalance(); } private void prune(PriorityQueue<Integer> pq) { while (!pq.isEmpty() && delayed.containsKey(pq.peek())) { if (delayed.merge(pq.peek(), -1, Integer::sum) == 0) { delayed.remove(pq.peek()); } pq.poll(); } } private void rebalance() { if (smallSize > largeSize + 1) { large.offer(small.poll()); --smallSize; ++largeSize; prune(small); } else if (smallSize < largeSize) { small.offer(large.poll()); --largeSize; ++smallSize; prune(large); } } } class Solution { public double[] medianSlidingWindow(int[] nums, int k) { MedianFinder finder = new MedianFinder(k); for (int i = 0; i < k; ++i) { finder.addNum(nums[i]); } int n = nums.length; double[] ans = new double[n - k + 1]; ans[0] = finder.findMedian(); for (int i = k; i < n; ++i) { finder.addNum(nums[i]); finder.removeNum(nums[i - k]); ans[i - k + 1] = finder.findMedian(); } return ans; } }
神奇字符串 s 仅由 '1' 和 '2' 组成,并需要遵守下面的规则:
'1' 和 '2' 的连续出现次数可以生成该字符串。s 的前几个元素是 s = "1221121221221121122……" 。如果将 s 中连续的若干 1 和 2 进行分组,可以得到 "1 22 11 2 1 22 1 22 11 2 11 22 ......" 。每组中 1 或者 2 的出现次数分别是 "1 2 2 1 1 2 1 2 2 1 2 2 ......" 。上面的出现次数正是 s 自身。
给你一个整数 n ,返回在神奇字符串 s 的前 n 个数字中 1 的数目。
示例 1:
输入:n = 6 输出:3 解释:神奇字符串 s 的前 6 个元素是 “122112”,它包含三个 1,因此返回 3 。
示例 2:
输入:n = 1 输出:1
提示:
1 <= n <= 105方法一:模拟构造过程
根据题目,我们得知,字符串 的每一组数字都可以由字符串 自身的数字得到。
字符串 前两组数字为 和 ,是由字符串 的第一个数字 和第二个数字 得到的,并且第一组数字只包含 ,第二组数字只包含 ,第三组数字只包含 ,以此类推。
由于前两组数字已知,我们初始化字符串 为 ,然后从第三组开始构造,第三组数字是由字符串 的第三个数字(下标 )得到,因此我们此时将指针 指向字符串 的第三个数字 。
1 2 2
^
i
指针 指向的数字为 ,表示第三组的数字会出现两次,并且,由于前一组数字为 ,组之间数字交替出现,因此第三组数字为两个 ,即 。构造后,指针 移动到下一个位置,即指向字符串 的第四个数字 。
1 2 2 1 1
^
i
这时候指针 指向的数字为 ,表示第四组的数字会出现一次,并且,由于前一组数字为 ,组之间数字交替出现,因此第四组数字为一个 ,即 。构造后,指针 移动到下一个位置,即指向字符串 的第五个数字 。
1 2 2 1 1 2
^
i
我们按照这个规则,依次模拟构造过程,直到字符串 的长度大于等于 。
时间复杂度 ,空间复杂度 。
class Solution { public int magicalString(int n) { List<Integer> s = new ArrayList<>(Arrays.asList(1, 2, 2)); for (int i = 2; s.size() < n; ++i) { int pre = s.get(s.size() - 1); int cur = 3 - pre; for (int j = 0; j < s.get(i); ++j) { s.add(cur); } } int ans = 0; for (int i = 0; i < n; ++i) { if (s.get(i) == 1) { ++ans; } } return ans; } }
给定一个许可密钥字符串 s,仅由字母、数字字符和破折号组成。字符串由 n 个破折号分成 n + 1 组。你也会得到一个整数 k 。
我们想要重新格式化字符串 s,使每一组包含 k 个字符,除了第一组,它可以比 k 短,但仍然必须包含至少一个字符。此外,两组之间必须插入破折号,并且应该将所有小写字母转换为大写字母。
返回 重新格式化的许可密钥 。
示例 1:
输入:S = "5F3Z-2e-9-w", k = 4 输出:"5F3Z-2E9W" 解释:字符串 S 被分成了两个部分,每部分 4 个字符; 注意,两个额外的破折号需要删掉。
示例 2:
输入:S = "2-5g-3-J", k = 2 输出:"2-5G-3J" 解释:字符串 S 被分成了 3 个部分,按照前面的规则描述,第一部分的字符可以少于给定的数量,其余部分皆为 2 个字符。
提示:
1 <= s.length <= 105s 只包含字母、数字和破折号 '-'.1 <= k <= 104简单模拟。
class Solution { public String licenseKeyFormatting(String s, int k) { s = s.replace("-", "").toUpperCase(); StringBuilder sb = new StringBuilder(); int t = 0; int cnt = s.length() % k; if (cnt == 0) { cnt = k; } for (int i = 0; i < s.length(); ++i) { sb.append(s.charAt(i)); ++t; if (t == cnt) { t = 0; cnt = k; if (i != s.length() - 1) { sb.append('-'); } } } return sb.toString(); } }
以字符串的形式给出 n , 以字符串的形式返回 n 的最小 好进制 。
如果 n 的 k(k>=2) 进制数的所有数位全为1,则称 k(k>=2) 是 n 的一个 好进制 。
示例 1:
输入:n = "13" 输出:"3" 解释:13 的 3 进制是 111。
示例 2:
输入:n = "4681" 输出:"8" 解释:4681 的 8 进制是 11111。
示例 3:
输入:n = "1000000000000000000" 输出:"999999999999999999" 解释:1000000000000000000 的 999999999999999999 进制是 11。
提示:
n 的取值范围是 [3, 1018]n 没有前导 0方法一:数学
假设 在 进制下的所有位数均为 ,且位数为 ,那么有式子 ①:
当 时,上式 ,而题目 取值范围为 ,因此 。
当 时,上式 ,即 。
我们来证明一般情况下的两个结论,以帮助解决本题。
结论一:
注意到式子 ① 是个首项为 ,且公比为 的等比数列。利用等比数列求和公式,我们可以得出:
变形得:
移项得:
题目 取值范围为 ,又因为 ,因此 。
结论二:
根据二项式定理:
整合,可得:
当 时,满足:
所以有:
即:
由于 是整数,因此 。
综上,依据结论一,我们知道 的取值范围为 ,且 时必然有解。随着 的增大,进制 不断减小。所以我们只需要从大到小检查每一个 可能的取值,利用结论二快速算出对应的 值,然后校验计算出的 值是否有效即可。如果 值有效,我们即可返回结果。
时间复杂度 。
class Solution { public String smallestGoodBase(String n) { long num = Long.parseLong(n); for (int len = 63; len >= 2; --len) { long radix = getRadix(len, num); if (radix != -1) { return String.valueOf(radix); } } return String.valueOf(num - 1); } private long getRadix(int len, long num) { long l = 2, r = num - 1; while (l < r) { long mid = l + r >>> 1; if (calc(mid, len) >= num) r = mid; else l = mid + 1; } return calc(r, len) == num ? r : -1; } private long calc(long radix, int len) { long p = 1; long sum = 0; for (int i = 0; i < len; ++i) { if (Long.MAX_VALUE - sum < p) { return Long.MAX_VALUE; } sum += p; if (Long.MAX_VALUE / p < radix) { p = Long.MAX_VALUE; } else { p *= radix; } } return sum; } }
由范围 [1,n] 内所有整数组成的 n 个整数的排列 perm 可以表示为长度为 n - 1 的字符串 s ,其中:
perm[i] < perm[i + 1] ,那么 s[i] == ' i 'perm[i] > perm[i + 1] ,那么 s[i] == 'D' 。给定一个字符串 s ,重构字典序上最小的排列 perm 并返回它。
示例 1:
输入: s = "I" 输出: [1,2] 解释: [1,2] 是唯一合法的可以生成秘密签名 "I" 的特定串,数字 1 和 2 构成递增关系。
示例 2:
输入: s = "DI" 输出: [2,1,3] 解释: [2,1,3] 和 [3,1,2] 可以生成秘密签名 "DI", 但是由于我们要找字典序最小的排列,因此你需要输出 [2,1,3]。
提示:
1 <= s.length <= 105s[i] 只会包含字符 'D' 和 'I'。方法一:贪心
先初始化结果数组 ans 为 [1, 2, 3, ..., n+1]。
假定某个连续 D 子数组区间为 [i, j),那么只要翻转 ans[i: j + 1] 即可。
因此,遍历字符串 s,找出所有的连续 D 子数组区间,将其翻转。
时间复杂度 ,其中 表示字符串 s 的长度。
class Solution { public int[] findPermutation(String s) { int n = s.length(); int[] ans = new int[n + 1]; for (int i = 0; i < n + 1; ++i) { ans[i] = i + 1; } int i = 0; while (i < n) { int j = i; while (j < n && s.charAt(j) == 'D') { ++j; } reverse(ans, i, j); i = Math.max(i + 1, j); } return ans; } private void reverse(int[] arr, int i, int j) { for (; i < j; ++i, --j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } }
给定一个二进制数组 nums , 计算其中最大连续 1 的个数。
示例 1:
输入:nums = [1,1,0,1,1,1] 输出:3 解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
示例 2:
输入:nums = [1,0,1,1,0,1] 输出:2
提示:
1 <= nums.length <= 105nums[i] 不是 0 就是 1.方法一:一次遍历
遍历数组,记录当前连续 的个数 cnt,以及最大连续 的个数 ans。如果当前元素为 ,则 cnt++,否则更新 ans,并且 cnt=0。最后返回 max(ans, cnt) 即可。
时间复杂度 ,空间复杂度 。其中 为数组 nums 的长度。
class Solution { public int findMaxConsecutiveOnes(int[] nums) { int cnt = 0, ans = 0; for (int v : nums) { if (v == 1) { ++cnt; } else { ans = Math.max(ans, cnt); cnt = 0; } } return Math.max(cnt, ans); } }
给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。
玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。
如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。
示例 1:
输入:nums = [1,5,2] 输出:false 解释:一开始,玩家 1 可以从 1 和 2 中进行选择。 如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。 所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。 因此,玩家 1 永远不会成为赢家,返回 false 。
示例 2:
输入:nums = [1,5,233,7] 输出:true 解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。 最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。
提示:
1 <= nums.length <= 200 <= nums[i] <= 107方法一:记忆化搜索
我们设计一个函数 ,表示从第 个数到第 个数,当前玩家与另一个玩家的得分之差的最大值。那么答案就是 。
函数 的计算方法如下:
最后,我们只需要判断 即可。
为了避免重复计算,我们可以使用记忆化搜索的方法,用一个数组 记录所有的 的值,当函数再次被调用到时,我们可以直接从 中取出答案而不需要重新计算。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
方法二:动态规划
我们也可以使用动态规划的方法,定义 表示当前玩家在 这些数字中能够获得的最大得分的差值。那么最后答案就是 。
初始时 ,因为只有一个数,所以当前玩家只能拿取这个数,得分差值为 。
考虑 ,其中 ,有两种情况:
因此,最终的状态转移方程为 。
最后,我们只需要判断 即可。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
相似题目:
class Solution { private int[] nums; private int[][] f; public boolean PredictTheWinner(int[] nums) { this.nums = nums; int n = nums.length; f = new int[n][n]; return dfs(0, n - 1) >= 0; } private int dfs(int i, int j) { if (i > j) { return 0; } if (f[i][j] != 0) { return f[i][j]; } return f[i][j] = Math.max(nums[i] - dfs(i + 1, j), nums[j] - dfs(i, j - 1)); } }
class Solution { public boolean PredictTheWinner(int[] nums) { int n = nums.length; int[][] f = new int[n][n]; for (int i = 0; i < n; ++i) { f[i][i] = nums[i]; } for (int i = n - 2; i >= 0; --i) { for (int j = i + 1; j < n; ++j) { f[i][j] = Math.max(nums[i] - f[i + 1][j], nums[j] - f[i][j - 1]); } } return f[0][n - 1] >= 0; } }
给定一个二进制数组 nums ,如果最多可以翻转一个 0 ,则返回数组中连续 1 的最大个数。
示例 1:
输入:nums = [1,0,1,1,0] 输出:4 解释:翻转第一个 0 可以得到最长的连续 1。 当翻转以后,最大连续 1 的个数为 4。
示例 2:
输入:nums = [1,0,1,1,0,1] 输出:4
提示:
1 <= nums.length <= 105nums[i] 不是 0 就是 1.进阶:如果输入的数字是作为 无限流 逐个输入如何处理?换句话说,内存不能存储下所有从流中输入的数字。您可以有效地解决吗?
方法一:预处理 + 枚举
定义 left, right 数组表示以第 个元素结尾(开头),往前(往后)累计的最大连续 的个数。
先遍历 nums,预处理出 left 和 right。
然后枚举 nums 每个位置 ,统计以 为分界点,左右两边最大连续 的个数之和,取最大值即可。
时间复杂度 ,空间复杂度 。其中 为 nums 的长度。
方法二:滑动窗口
找出最大的窗口,使得窗口内的 的个数不超过 个。
时间复杂度 ,空间复杂度 。其中 为 nums 的长度。
相似题目:1004. 最大连续 1 的个数 III
以下是滑动窗口的优化版本。
维护一个单调变长的窗口。这种窗口经常出现在寻求“最大窗口”的问题中:因为求的是“最大”,所以我们没有必要缩短窗口,于是代码就少了缩短窗口的部分;从另一个角度讲,本题里的 K 是资源数,一旦透支,窗口就不能再增长了。
r++ 每次都会执行,l++ 只有资源 k < 0 时才触发,因此 r - l 的值只会单调递增(或保持不变)class Solution { public int findMaxConsecutiveOnes(int[] nums) { int n = nums.length; int[] left = new int[n]; int[] right = new int[n]; for (int i = 0; i < n; ++i) { if (nums[i] == 1) { left[i] = i == 0 ? 1 : left[i - 1] + 1; } } for (int i = n - 1; i >= 0; --i) { if (nums[i] == 1) { right[i] = i == n - 1 ? 1 : right[i + 1] + 1; } } int ans = 0; for (int i = 0; i < n; ++i) { int t = 0; if (i > 0) { t += left[i - 1]; } if (i < n - 1) { t += right[i + 1]; } ans = Math.max(ans, t + 1); } return ans; } }
class Solution { public int findMaxConsecutiveOnes(int[] nums) { int j = 0, cnt = 0; int ans = 1; for (int i = 0; i < nums.length; ++i) { if (nums[i] == 0) { ++cnt; } while (cnt > 1) { if (nums[j++] == 0) { --cnt; } } ans = Math.max(ans, i - j + 1); } return ans; } }
class Solution { public int findMaxConsecutiveOnes(int[] nums) { int l = 0, r = 0; int k = 1; while (r < nums.length) { if (nums[r++] == 0) { --k; } if (k < 0 && nums[l++] == 0) { ++k; } } return r - l; } }
你正在参与祖玛游戏的一个变种。
在这个祖玛游戏变体中,桌面上有 一排 彩球,每个球的颜色可能是:红色 'R'、黄色 'Y'、蓝色 'B'、绿色 'G' 或白色 'W' 。你的手中也有一些彩球。
你的目标是 清空 桌面上所有的球。每一回合:
给你一个字符串 board ,表示桌面上最开始的那排球。另给你一个字符串 hand ,表示手里的彩球。请你按上述操作步骤移除掉桌上所有球,计算并返回所需的 最少 球数。如果不能移除桌上所有的球,返回 -1 。
示例 1:
输入:board = "WRRBBW", hand = "RB" 输出:-1 解释:无法移除桌面上的所有球。可以得到的最好局面是: - 插入一个 'R' ,使桌面变为 WRRRBBW 。WRRRBBW -> WBBW - 插入一个 'B' ,使桌面变为 WBBBW 。WBBBW -> WW 桌面上还剩着球,没有其他球可以插入。
示例 2:
输入:board = "WWRRBBWW", hand = "WRBRW" 输出:2 解释:要想清空桌面上的球,可以按下述步骤: - 插入一个 'R' ,使桌面变为 WWRRRBBWW 。WWRRRBBWW -> WWBBWW - 插入一个 'B' ,使桌面变为 WWBBBWW 。WWBBBWW -> WWWW -> empty 只需从手中出 2 个球就可以清空桌面。
示例 3:
输入:board = "G", hand = "GGGGG" 输出:2 解释:要想清空桌面上的球,可以按下述步骤: - 插入一个 'G' ,使桌面变为 GG 。 - 插入一个 'G' ,使桌面变为 GGG 。GGG -> empty 只需从手中出 2 个球就可以清空桌面。
示例 4:
输入:board = "RBYYBBRRB", hand = "YRBGB" 输出:3 解释:要想清空桌面上的球,可以按下述步骤: - 插入一个 'Y' ,使桌面变为 RBYYYBBRRB 。RBYYYBBRRB -> RBBBRRB -> RRRB -> B - 插入一个 'B' ,使桌面变为 BB 。 - 插入一个 'B' ,使桌面变为 BBB 。BBB -> empty 只需从手中出 3 个球就可以清空桌面。
提示:
1 <= board.length <= 161 <= hand.length <= 5board 和 hand 由字符 'R'、'Y'、'B'、'G' 和 'W' 组成房间(用格栅表示)中有一个扫地机器人。格栅中的每一个格子有空和障碍物两种可能。
扫地机器人提供4个API,可以向前进,向左转或者向右转。每次转弯90度。
当扫地机器人试图进入障碍物格子时,它的碰撞传感器会探测出障碍物,使它停留在原地。
请利用提供的4个API编写让机器人清理整个房间的算法。
interface Robot {
// 若下一个方格为空,则返回true,并移动至该方格
// 若下一个方格为障碍物,则返回false,并停留在原地
boolean move();
// 在调用turnLeft/turnRight后机器人会停留在原位置
// 每次转弯90度
void turnLeft();
void turnRight();
// 清理所在方格
void clean();
}
示例:
输入: room = [ [1,1,1,1,1,0,1,1], [1,1,1,1,1,0,1,1], [1,0,1,1,1,1,1,1], [0,0,0,1,0,0,0,0], [1,1,1,1,1,1,1,1] ], row = 1, col = 3 解析: 房间格栅用0或1填充。0表示障碍物,1表示可以通过。 机器人从row=1,col=3的初始位置出发。在左上角的一行以下,三列以右。
注意:
DFS。
我们设定机器人起始位置 (0, 0),朝向 d = 0。
将起始位置进行清扫,并进行标记(即清扫过的格子也算作障碍);然后依次选择四个朝向 up,right,down 和 left 进行深度优先搜索,相邻的两个朝向仅差一次向右旋转的操作;
如果四个朝向都搜索完毕,则回溯到上一次搜索。
/** * // This is the robot's control interface. * // You should not implement it, or speculate about its implementation * interface Robot { * // Returns true if the cell in front is open and robot moves into the cell. * // Returns false if the cell in front is blocked and robot stays in the current cell. * public boolean move(); * * // Robot will stay in the same cell after calling turnLeft/turnRight. * // Each turn will be 90 degrees. * public void turnLeft(); * public void turnRight(); * * // Clean the current cell. * public void clean(); * } */ class Solution { private Set<String> vis; private int[][] dirs = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; public void cleanRoom(Robot robot) { vis = new HashSet<>(); dfs(0, 0, 0, robot); } private void dfs(int i, int j, int d, Robot robot) { vis.add(i + "," + j); robot.clean(); for (int k = 0; k < 4; ++k) { int nd = (d + k) % 4; int x = i + dirs[nd][0]; int y = j + dirs[nd][1]; if (!vis.contains(x + "," + y) && robot.move()) { dfs(x, y, nd, robot); back(robot); } robot.turnRight(); } } private void back(Robot robot) { robot.turnRight(); robot.turnRight(); robot.move(); robot.turnRight(); robot.turnRight(); } }
由空地(用 0 表示)和墙(用 1 表示)组成的迷宫 maze 中有一个球。球可以途经空地向 上、下、左、右 四个方向滚动,且在遇到墙壁前不会停止滚动。当球停下时,可以选择向下一个方向滚动。
给你一个大小为 m x n 的迷宫 maze ,以及球的初始位置 start 和目的地 destination ,其中 start = [startrow, startcol] 且 destination = [destinationrow, destinationcol] 。请你判断球能否在目的地停下:如果可以,返回 true ;否则,返回 false 。
你可以 假定迷宫的边缘都是墙壁(参考示例)。
示例 1:
输入:maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [4,4] 输出:true 解释:一种可能的路径是 : 左 -> 下 -> 左 -> 下 -> 右 -> 下 -> 右。
示例 2:
输入:maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [3,2] 输出:false 解释:不存在能够使球停在目的地的路径。注意,球可以经过目的地,但无法在那里停驻。
示例 3:
输入:maze = [[0,0,0,0,0],[1,1,0,0,1],[0,0,0,0,0],[0,1,0,0,1],[0,1,0,0,0]], start = [4,3], destination = [0,1] 输出:false
提示:
m == maze.lengthn == maze[i].length1 <= m, n <= 100maze[i][j] is 0 or 1.start.length == 2destination.length == 20 <= startrow, destinationrow <= m0 <= startcol, destinationcol <= nDFS 或 BFS。
DFS:
BFS:
DFS:
class Solution { private boolean[][] vis; private int[][] maze; private int[] d; private int m; private int n; public boolean hasPath(int[][] maze, int[] start, int[] destination) { m = maze.length; n = maze[0].length; d = destination; this.maze = maze; vis = new boolean[m][n]; dfs(start[0], start[1]); return vis[d[0]][d[1]]; } private void dfs(int i, int j) { if (vis[i][j]) { return; } vis[i][j] = true; if (i == d[0] && j == d[1]) { return; } int[] dirs = {-1, 0, 1, 0, -1}; for (int k = 0; k < 4; ++k) { int x = i, y = j; int a = dirs[k], b = dirs[k + 1]; while (x + a >= 0 && x + a < m && y + b >= 0 && y + b < n && maze[x + a][y + b] == 0) { x += a; y += b; } dfs(x, y); } } }
BFS:
class Solution { public boolean hasPath(int[][] maze, int[] start, int[] destination) { int m = maze.length; int n = maze[0].length; boolean[][] vis = new boolean[m][n]; vis[start[0]][start[1]] = true; Deque<int[]> q = new LinkedList<>(); q.offer(start); int[] dirs = {-1, 0, 1, 0, -1}; while (!q.isEmpty()) { int[] p = q.poll(); int i = p[0], j = p[1]; for (int k = 0; k < 4; ++k) { int x = i, y = j; int a = dirs[k], b = dirs[k + 1]; while ( x + a >= 0 && x + a < m && y + b >= 0 && y + b < n && maze[x + a][y + b] == 0) { x += a; y += b; } if (x == destination[0] && y == destination[1]) { return true; } if (!vis[x][y]) { vis[x][y] = true; q.offer(new int[] {x, y}); } } } return false; } }
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1] 输出:[[4,4]]
提示:
1 <= nums.length <= 15-100 <= nums[i] <= 100方法一:DFS
DFS 递归枚举每个数字选中或不选中,这里需要满足两个条件:
class Solution { private int[] nums; private List<List<Integer>> ans; public List<List<Integer>> findSubsequences(int[] nums) { this.nums = nums; ans = new ArrayList<>(); dfs(0, -1000, new ArrayList<>()); return ans; } private void dfs(int u, int last, List<Integer> t) { if (u == nums.length) { if (t.size() > 1) { ans.add(new ArrayList<>(t)); } return; } if (nums[u] >= last) { t.add(nums[u]); dfs(u + 1, nums[u], t); t.remove(t.size() - 1); } if (nums[u] != last) { dfs(u + 1, last, t); } } }
作为一位web开发者, 懂得怎样去规划一个页面的尺寸是很重要的。 所以,现给定一个具体的矩形页面面积,你的任务是设计一个长度为 L 和宽度为 W 且满足以下要求的矩形的页面。要求:
W 不应大于长度 L ,换言之,要求 L >= W 。L 和宽度 W 之间的差距应当尽可能小。返回一个 数组 [L, W],其中 L 和 W 是你按照顺序设计的网页的长度和宽度。
示例1:
输入: 4 输出: [2, 2] 解释: 目标面积是 4, 所有可能的构造方案有 [1,4], [2,2], [4,1]。 但是根据要求2,[1,4] 不符合要求; 根据要求3,[2,2] 比 [4,1] 更能符合要求. 所以输出长度 L 为 2, 宽度 W 为 2。
示例 2:
输入: area = 37 输出: [37,1]
示例 3:
输入: area = 122122 输出: [427,286]
提示:
1 <= area <= 107class Solution { public int[] constructRectangle(int area) { int w = (int) Math.sqrt(area); while (area % w != 0) { --w; } return new int[] {area / w, w}; } }
给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
输入: [1,3,2,3,1] 输出: 2
示例 2:
输入: [2,4,3,5,1] 输出: 3
注意:
50000。方法一:归并排序
归并排序的过程中,如果左边的数大于右边的数,则右边的数与左边的数之后的数都构成逆序对。
时间复杂度 ,空间复杂度 。其中 为数组长度。
方法二:树状数组
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
update(x, delta): 把序列 x 位置的数加上一个值 delta;query(x):查询序列 [1,...x] 区间的区间和,即位置 x 的前缀和。这两个操作的时间复杂度均为 。
树状数组最基本的功能就是求比某点 x 小的点的个数(这里的比较是抽象的概念,可以是数的大小、坐标的大小、质量的大小等等)。
比如给定数组 a[5] = {2, 5, 3, 4, 1},求 b[i] = 位置 i 左边小于等于 a[i] 的数的个数。对于此例,b[5] = {0, 1, 1, 2, 0}。
解决方案是直接遍历数组,每个位置先求出 query(a[i]),然后再修改树状数组 update(a[i], 1) 即可。当数的范围比较大时,需要进行离散化,即先进行去重并排序,然后对每个数字进行编号。
方法三:线段树
线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。
[1, N];[x, x];[l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r], 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。归并排序:
树状数组:
线段树:
归并排序:
class Solution { private int[] nums; private int[] t; public int reversePairs(int[] nums) { this.nums = nums; int n = nums.length; this.t = new int[n]; return mergeSort(0, n - 1); } private int mergeSort(int l, int r) { if (l >= r) { return 0; } int mid = (l + r) >> 1; int ans = mergeSort(l, mid) + mergeSort(mid + 1, r); int i = l, j = mid + 1, k = 0; while (i <= mid && j <= r) { if (nums[i] <= nums[j] * 2L) { ++i; } else { ans += mid - i + 1; ++j; } } i = l; j = mid + 1; while (i <= mid && j <= r) { if (nums[i] <= nums[j]) { t[k++] = nums[i++]; } else { t[k++] = nums[j++]; } } while (i <= mid) { t[k++] = nums[i++]; } while (j <= r) { t[k++] = nums[j++]; } for (i = l; i <= r; ++i) { nums[i] = t[i - l]; } return ans; } }
树状数组:
class Solution { public int reversePairs(int[] nums) { TreeSet<Long> ts = new TreeSet<>(); for (int num : nums) { ts.add((long) num); ts.add((long) num * 2); } Map<Long, Integer> m = new HashMap<>(); int idx = 0; for (long num : ts) { m.put(num, ++idx); } BinaryIndexedTree tree = new BinaryIndexedTree(m.size()); int ans = 0; for (int i = nums.length - 1; i >= 0; --i) { int x = m.get((long) nums[i]); ans += tree.query(x - 1); tree.update(m.get((long) nums[i] * 2), 1); } return ans; } } class BinaryIndexedTree { private int n; private int[] c; public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; } public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } } public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; } public static int lowbit(int x) { return x & -x; } }
线段树:
class Solution { public int reversePairs(int[] nums) { TreeSet<Long> ts = new TreeSet<>(); for (int num : nums) { ts.add((long) num); ts.add((long) num * 2); } Map<Long, Integer> m = new HashMap<>(); int idx = 0; for (long num : ts) { m.put(num, ++idx); } SegmentTree tree = new SegmentTree(m.size()); int ans = 0; for (int i = nums.length - 1; i >= 0; --i) { int x = m.get((long) nums[i]); ans += tree.query(1, 1, x - 1); tree.modify(1, m.get((long) nums[i] * 2), 1); } return ans; } } class Node { int l; int r; int v; } class SegmentTree { private Node[] tr; public SegmentTree(int n) { tr = new Node[4 * n]; for (int i = 0; i < tr.length; ++i) { tr[i] = new Node(); } build(1, 1, n); } public void build(int u, int l, int r) { tr[u].l = l; tr[u].r = r; if (l == r) { return; } int mid = (l + r) >> 1; build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r); } public void modify(int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) { tr[u].v += v; return; } int mid = (tr[u].l + tr[u].r) >> 1; if (x <= mid) { modify(u << 1, x, v); } else { modify(u << 1 | 1, x, v); } pushup(u); } public void pushup(int u) { tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v; } public int query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) { return tr[u].v; } int mid = (tr[u].l + tr[u].r) >> 1; int v = 0; if (l <= mid) { v += query(u << 1, l, r); } if (r > mid) { v += query(u << 1 | 1, l, r); } return v; } }
归并排序:
树状数组:
线段树:
归并排序:
树状数组:
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1 输出:1
提示:
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000方法一:动态规划
题目可以转换为 0-1 背包问题。
设整数数组总和为 s,添加 - 号的元素之和为 x,则添加 + 号的元素之和为 s - x,那么 s - x - x = target,2x = s - target。左式成立需要满足 s - target 一定大于等于 0,并且能够被 2 整除。在此前提下,我们可以将问题抽象为: 从数组中选出若干个数,使得选出的元素之和为 x。显然这是一个 0-1 背包问题。
定义 dp[i][j] 表示从前 i 个数中选出若干个数,使得所选元素之和为 j 的所有方案数。
动态规划——0-1 背包朴素做法:
动态规划——0-1 背包空间优化:
DFS:
class Solution { public int findTargetSumWays(int[] nums, int target) { int s = 0; for (int v : nums) { s += v; } if (s < target || (s - target) % 2 != 0) { return 0; } int m = nums.length; int n = (s - target) / 2; int[][] dp = new int[m + 1][n + 1]; dp[0][0] = 1; for (int i = 1; i <= m; ++i) { for (int j = 0; j <= n; ++j) { dp[i][j] = dp[i - 1][j]; if (nums[i - 1] <= j) { dp[i][j] += dp[i - 1][j - nums[i - 1]]; } } } return dp[m][n]; } }
class Solution { public int findTargetSumWays(int[] nums, int target) { int s = 0; for (int v : nums) { s += v; } if (s < target || (s - target) % 2 != 0) { return 0; } int n = (s - target) / 2; int[] dp = new int[n + 1]; dp[0] = 1; for (int v : nums) { for (int j = n; j >= v; --j) { dp[j] += dp[j - v]; } } return dp[n]; } }
在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄。他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。
当提莫攻击艾希,艾希的中毒状态正好持续 duration 秒。
正式地讲,提莫在 t 发起攻击意味着艾希在时间区间 [t, t + duration - 1](含 t 和 t + duration - 1)处于中毒状态。如果提莫在中毒影响结束 前 再次攻击,中毒状态计时器将会 重置 ,在新的攻击之后,中毒影响将会在 duration 秒后结束。
给你一个 非递减 的整数数组 timeSeries ,其中 timeSeries[i] 表示提莫在 timeSeries[i] 秒时对艾希发起攻击,以及一个表示中毒持续时间的整数 duration 。
返回艾希处于中毒状态的 总 秒数。
示例 1:
输入:timeSeries = [1,4], duration = 2 输出:4 解释:提莫攻击对艾希的影响如下: - 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。 - 第 4 秒,提莫再次攻击艾希,艾希中毒状态又持续 2 秒,即第 4 秒和第 5 秒。 艾希在第 1、2、4、5 秒处于中毒状态,所以总中毒秒数是 4 。
示例 2:
输入:timeSeries = [1,2], duration = 2 输出:3 解释:提莫攻击对艾希的影响如下: - 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。 - 第 2 秒,提莫再次攻击艾希,并重置中毒计时器,艾希中毒状态需要持续 2 秒,即第 2 秒和第 3 秒。 艾希在第 1、2、3 秒处于中毒状态,所以总中毒秒数是 3 。
提示:
1 <= timeSeries.length <= 1040 <= timeSeries[i], duration <= 107timeSeries 按 非递减 顺序排列方法一:一次遍历
我们先考虑最后一次攻击,此次攻击一定可以使得艾希处于中毒状态,所以总中毒时间至少为 duration。
接下来,我们考虑前 次攻击,每一次攻击的中毒持续时间为 ,其中 从 1 开始。我们将这些中毒持续时间累加起来,即为总中毒时间。
时间复杂度 ,空间复杂度 。其中 为数组 timeSeries 的长度。
class Solution { public int findPoisonedDuration(int[] timeSeries, int duration) { int n = timeSeries.length; int ans = duration; for (int i = 1; i < n; ++i) { ans += Math.min(duration, timeSeries[i] - timeSeries[i - 1]); } return ans; } }
nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。
给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。
对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。
返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。
示例 1:
输入:nums1 = [4,1,2], nums2 = [1,3,4,2]. 输出:[-1,3,-1] 解释:nums1 中每个值的下一个更大元素如下所述: - 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。 - 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。 - 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
示例 2:
输入:nums1 = [2,4], nums2 = [1,2,3,4]. 输出:[3,-1] 解释:nums1 中每个值的下一个更大元素如下所述: - 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。 - 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。
提示:
1 <= nums1.length <= nums2.length <= 10000 <= nums1[i], nums2[i] <= 104nums1和nums2中所有整数 互不相同nums1 中的所有整数同样出现在 nums2 中进阶:你可以设计一个时间复杂度为 O(nums1.length + nums2.length) 的解决方案吗?
方法一:单调栈
单调栈常见模型:找出每个数左/右边离它最近的且比它大/小的数。模板:
对于本题,先对将 nums2 中的每一个元素,求出其下一个更大的元素。随后对于将这些答案放入哈希表 中,再遍历数组 nums1,并直接找出答案。对于 nums2,可以使用单调栈来解决这个问题。
时间复杂度 ,其中 和 分别为数组 nums1 和 nums2 的长度。
class Solution { public int[] nextGreaterElement(int[] nums1, int[] nums2) { Deque<Integer> stk = new ArrayDeque<>(); Map<Integer, Integer> m = new HashMap<>(); for (int v : nums2) { while (!stk.isEmpty() && stk.peek() < v) { m.put(stk.pop(), v); } stk.push(v); } int n = nums1.length; int[] ans = new int[n]; for (int i = 0; i < n; ++i) { ans[i] = m.getOrDefault(nums1[i], -1); } return ans; } }
class Solution { public int[] nextGreaterElement(int[] nums1, int[] nums2) { Deque<Integer> stk = new ArrayDeque<>(); Map<Integer, Integer> m = new HashMap<>(); for (int i = nums2.length - 1; i >= 0; --i) { while (!stk.isEmpty() && stk.peek() <= nums2[i]) { stk.pop(); } if (!stk.isEmpty()) { m.put(nums2[i], stk.peek()); } stk.push(nums2[i]); } int n = nums1.length; int[] ans = new int[n]; for (int i = 0; i < n; ++i) { ans[i] = m.getOrDefault(nums1[i], -1); } return ans; } }
给定一个由非重叠的轴对齐矩形的数组 rects ,其中 rects[i] = [ai, bi, xi, yi] 表示 (ai, bi) 是第 i 个矩形的左下角点,(xi, yi) 是第 i 个矩形的右上角点。设计一个算法来随机挑选一个被某一矩形覆盖的整数点。矩形周长上的点也算做是被矩形覆盖。所有满足要求的点必须等概率被返回。
在给定的矩形覆盖的空间内的任何整数点都有可能被返回。
请注意 ,整数点是具有整数坐标的点。
实现 Solution 类:
Solution(int[][] rects) 用给定的矩形数组 rects 初始化对象。int[] pick() 返回一个随机的整数点 [u, v] 在给定的矩形所覆盖的空间内。示例 1:

输入: ["Solution", "pick", "pick", "pick", "pick", "pick"] [[[[-2, -2, 1, 1], [2, 2, 4, 6]]], [], [], [], [], []] 输出: [null, [1, -2], [1, -1], [-1, -2], [-2, -2], [0, 0]] 解释: Solution solution = new Solution([[-2, -2, 1, 1], [2, 2, 4, 6]]); solution.pick(); // 返回 [1, -2] solution.pick(); // 返回 [1, -1] solution.pick(); // 返回 [-1, -2] solution.pick(); // 返回 [-2, -2] solution.pick(); // 返回 [0, 0]
提示:
1 <= rects.length <= 100rects[i].length == 4-109 <= ai < xi <= 109-109 <= bi < yi <= 109xi - ai <= 2000yi - bi <= 2000pick 最多被调用 104 次。方法一:前缀和 + 二分查找
将矩形面积求前缀和 ,然后随机获取到一个面积 ,利用二分查找定位到是哪个矩形,然后继续随机获取该矩形的其中一个整数点坐标即可。
class Solution { private int[] s; private int[][] rects; private Random random = new Random(); public Solution(int[][] rects) { int n = rects.length; s = new int[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + (rects[i][2] - rects[i][0] + 1) * (rects[i][3] - rects[i][1] + 1); } this.rects = rects; } public int[] pick() { int n = rects.length; int v = 1 + random.nextInt(s[n]); int left = 0, right = n; while (left < right) { int mid = (left + right) >> 1; if (s[mid] >= v) { right = mid; } else { left = mid + 1; } } int[] rect = rects[left - 1]; return new int[] {rect[0] + random.nextInt(rect[2] - rect[0] + 1), rect[1] + random.nextInt(rect[3] - rect[1] + 1)}; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(rects); * int[] param_1 = obj.pick(); */
给你一个大小为 m x n 的矩阵 mat ,请以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。
示例 1:
输入:mat = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,4,7,5,3,6,8,9]
示例 2:
输入:mat = [[1,2],[3,4]] 输出:[1,2,3,4]
提示:
m == mat.lengthn == mat[i].length1 <= m, n <= 1041 <= m * n <= 104-105 <= mat[i][j] <= 105方法一:定点遍历
对于每一轮 ,我们固定从右上方开始往左下方遍历,得到 。如果 为偶数,再将 逆序。然后将 添加到结果数组 ans 中。
问题的关键在于确定轮数以及每一轮的起始坐标点 。
时间复杂度 。其中 和 分别为矩阵的行数和列数。
class Solution { public int[] findDiagonalOrder(int[][] mat) { int m = mat.length, n = mat[0].length; int[] ans = new int[m * n]; int idx = 0; List<Integer> t = new ArrayList<>(); for (int k = 0; k < m + n - 1; ++k) { int i = k < n ? 0 : k - n + 1; int j = k < n ? k : n - 1; while (i < m && j >= 0) { t.add(mat[i][j]); ++i; --j; } if (k % 2 == 0) { Collections.reverse(t); } for (int v : t) { ans[idx++] = v; } t.clear(); } return ans; } }
由空地和墙组成的迷宫中有一个球。球可以向上(u)下(d)左(l)右(r)四个方向滚动,但在遇到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。迷宫中还有一个洞,当球运动经过洞时,就会掉进洞里。
给定球的起始位置,目的地和迷宫,找出让球以最短距离掉进洞里的路径。 距离的定义是球从起始位置(不包括)到目的地(包括)经过的空地个数。通过'u', 'd', 'l' 和 'r'输出球的移动方向。 由于可能有多条最短路径, 请输出字典序最小的路径。如果球无法进入洞,输出"impossible"。
迷宫由一个0和1的二维数组表示。 1表示墙壁,0表示空地。你可以假定迷宫的边缘都是墙壁。起始位置和目的地的坐标通过行号和列号给出。
示例1:
输入 1: 迷宫由以下二维数组表示 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 输入 2: 球的初始位置 (rowBall, colBall) = (4, 3) 输入 3: 洞的位置 (rowHole, colHole) = (0, 1) 输出: "lul" 解析: 有两条让球进洞的最短路径。 第一条路径是 左 -> 上 -> 左, 记为 "lul". 第二条路径是 上 -> 左, 记为 'ul'. 两条路径都具有最短距离6, 但'l' < 'u',故第一条路径字典序更小。因此输出"lul"。![]()
示例 2:
输入 1: 迷宫由以下二维数组表示 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 输入 2: 球的初始位置 (rowBall, colBall) = (4, 3) 输入 3: 洞的位置 (rowHole, colHole) = (3, 0) 输出: "impossible" 示例: 球无法到达洞。![]()
注意:
方法一:BFS
class Solution { public String findShortestWay(int[][] maze, int[] ball, int[] hole) { int m = maze.length; int n = maze[0].length; int r = ball[0], c = ball[1]; int rh = hole[0], ch = hole[1]; Deque<int[]> q = new LinkedList<>(); q.offer(new int[] {r, c}); int[][] dist = new int[m][n]; for (int i = 0; i < m; ++i) { Arrays.fill(dist[i], Integer.MAX_VALUE); } dist[r][c] = 0; String[][] path = new String[m][n]; path[r][c] = ""; int[][] dirs = {{-1, 0, 'u'}, {1, 0, 'd'}, {0, -1, 'l'}, {0, 1, 'r'}}; while (!q.isEmpty()) { int[] p = q.poll(); int i = p[0], j = p[1]; for (int[] dir : dirs) { int a = dir[0], b = dir[1]; String d = String.valueOf((char) (dir[2])); int x = i, y = j; int step = dist[i][j]; while (x + a >= 0 && x + a < m && y + b >= 0 && y + b < n && maze[x + a][y + b] == 0 && (x != rh || y != ch)) { x += a; y += b; ++step; } if (dist[x][y] > step || (dist[x][y] == step && (path[i][j] + d).compareTo(path[x][y]) < 0)) { dist[x][y] = step; path[x][y] = path[i][j] + d; if (x != rh || y != ch) { q.offer(new int[] {x, y}); } } } } return path[rh][ch] == null ? "impossible" : path[rh][ch]; } }
给你一个字符串数组 words ,只返回可以使用在 美式键盘 同一行的字母打印出来的单词。键盘如下图所示。
美式键盘 中:
"qwertyuiop" 组成。"asdfghjkl" 组成。"zxcvbnm" 组成。
示例 1:
输入:words = ["Hello","Alaska","Dad","Peace"] 输出:["Alaska","Dad"]
示例 2:
输入:words = ["omk"] 输出:[]
示例 3:
输入:words = ["adsdf","sfd"] 输出:["adsdf","sfd"]
提示:
1 <= words.length <= 201 <= words[i].length <= 100words[i] 由英文字母(小写和大写字母)组成方法一:字符映射
我们将每个键盘行的字符映射到对应的行数,然后遍历字符串数组,判断每个字符串是否都在同一行即可。
时间复杂度 ,空间复杂度 。其中 为所有字符串的长度之和;而 为字符集的大小,本题中 。
class Solution { public String[] findWords(String[] words) { String s = "12210111011122000010020202"; List<String> ans = new ArrayList<>(); for (var w : words) { String t = w.toLowerCase(); char x = s.charAt(t.charAt(0) - 'a'); boolean ok = true; for (char c : t.toCharArray()) { if (s.charAt(c - 'a') != x) { ok = false; break; } } if (ok) { ans.add(w); } } return ans.toArray(new String[0]); } }